mall学习教程官网:macrozheng.com前言 多租户(Multi-Tenant)是SaaS中的一个重要概念,它是一种软件架构技术,在多个租户的环境下,共享同一套系统实例,并且租户之间的数据具有隔离性,也就是说一个租户不能去访问其他租户的数据。基于不同的隔离级别,通常具有下面三种实现方案:每个租户使用独立DataBase,隔离级别高,性能好,但成本大 租户之间共享DataBase,使用独立的Schema 租户之间共享Schema,在表上添加租户字段,共享数据程度最高,隔离级别最低。 数据库设计 Mybatis-plus在第3层隔离级别上,提供了基于分页插件的多租户的解决方案,我们对此来进行介绍。在正式开始前,首先做好准备工作创建两张表,在基础字段后都添加租户字段tenant_id:CREATE TABLE `user` ( `id` bigint(20) NOT NULL, `name` varchar(20) DEFAULT NULL, `phone` varchar(11) DEFAULT NULL, `address` varchar(64) DEFAULT NULL, `tenant_id` bigint(20) DEFAULT NULL, PRIMARY KEY (`id`) ) CREATE TABLE `dept` ( `id` bigint(20) NOT NULL, `dept_name` varchar(64) DEFAULT NULL, `comment` varchar(128) DEFAULT NULL, `tenant_id` bigint(20) DEFAULT NULL, PRIMARY KEY (`id`) )
这或许是一个对你有用的开源项目,mall项目是一套基于 SpringBoot3 + JDK 17 + Vue 实现的电商系统(Github标星60K),采用Docker容器化部署,后端支持多模块和微服务架构。包括前台商城项目和后台管理系统,能支持完整的订单流程!涵盖商品、订单、购物车、权限、优惠券、会员、支付等功能!Boot项目:https://github.com/macrozheng/mall Cloud项目:https://github.com/macrozheng/mall-swarm 视频 教程:https://www.macrozheng.com/video/ 项目演示:引入依赖 在项目中导入需要的依赖:<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.3.2</version> </dependency> <dependency> <groupId>com.github.jsqlparser</groupId> <artifactId>jsqlparser</artifactId> <version>3.1</version> </dependency>
实现 Mybatis-plus 配置类:@EnableTransactionManagement(proxyTargetClass = true) @Configuration public class MybatisPlusConfig { @Bean public PaginationInterceptor paginationInterceptor() { PaginationInterceptor paginationInterceptor = new PaginationInterceptor(); List<ISqlParser> sqlParserList=new ArrayList<>(); TenantSqlParser tenantSqlParser=new TenantSqlParser(); tenantSqlParser.setTenantHandler(new TenantHandler() { @Override public Expression getTenantId(boolean select) { String tenantId = "3"; return new StringValue(tenantId); } @Override public String getTenantIdColumn() { return "tenant_id"; } @Override public boolean doTableFilter(String tableName) { return false; } }); sqlParserList.add(tenantSqlParser); paginationInterceptor.setSqlParserList(sqlParserList); return paginationInterceptor; } }
这里主要实现的功能:创建SQL解析器集合 创建租户SQL解析器 设置租户处理器,具体处理租户逻辑 这里暂时把租户的id固定写成3,来进行测试 。测试执行全表语句:public List<User> getUserList() { return userMapper.selectList(new LambdaQueryWrapper<User>().isNotNull(User::getId)); }
使用插件解析执行的SQL语句,可以看到自动在查询条件后加上了租户过滤条件: 那么在实际的项目中,怎么将租户信息传给租户处理器呢,根据情况我们可以从缓存或者请求头中获取,以从Request请求头获取为例:@Override public Expression getTenantId(boolean select) { ServletRequestAttributes attributes=(ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); String tenantId = request.getHeader("tenantId"); return new StringValue(tenantId); }
前端在发起http请求时,在Header中加入tenantId字段,后端在处理器中获取后,设置为当前这次请求的租户过滤条件。 如果是基于请求头携带租户信息的情况,那么在使用中可能会遇到一个坑,如果当使用多线程的时候,新开启的异步线程并不会自动携带当前线程的Request请求。@Override public List<User> getUserListByFuture() { Callable getUser=()-> userMapper.selectList(new LambdaQueryWrapper<User>().isNotNull(User::getId)); FutureTask<List<User>> future=new FutureTask<>(getUser); new Thread(future).start(); try { return future.get(); } catch (Exception e) { e.printStackTrace(); } return null; }
执行上面的方法,可以看出是获取不到当前的Request请求的,因此无法获得租户id,会导致后续报错空指针异常: 修改的话也非常简单,开启RequestAttributes的子线程共享,修改上面的代码:@Override public List<User> getUserListByFuture() { ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); Callable getUser=()-> { RequestContextHolder.setRequestAttributes(sra, true); return userMapper.selectList(new LambdaQueryWrapper<User>().isNotNull(User::getId)); }; FutureTask<List<User>> future=new FutureTask<>(getUser); new Thread(future).start(); try { return future.get(); } catch (Exception e) { e.printStackTrace(); } return null; }
这样修改后,在异步线程中也能正常的获取租户信息了。 那么,有的小伙伴可能要问了,在业务中并不是所有的查询都需要过滤租户条件啊,针对这种情况,有两种方式来进行处理。 1、如果整张表的所有SQL操作都不需要针对租户进行操作,那么就对表进行过滤,修改doTableFilter方法,添加表的名称:@Override public boolean doTableFilter(String tableName) { List<String> IGNORE_TENANT_TABLES= Arrays.asList("dept"); return IGNORE_TENANT_TABLES.stream().anyMatch(e->e.equalsIgnoreCase(tableName)); }
这样,在dept表的所有查询都不进行过滤: 2、如果有一些特定的SQL语句不想被执行租户过滤,可以通过@SqlParser注解的形式开启,注意注解只能加在Mapper接口的方法上:@SqlParser(filter = true) @Select("select * from user where name =#{name}") User selectUserByName(@Param(value="name") String name);
或在分页拦截器中指定需要过滤的方法:@Bean public PaginationInterceptor paginationInterceptor() { PaginationInterceptor paginationInterceptor = new PaginationInterceptor(); paginationInterceptor.setSqlParserFilter(metaObject->{ MappedStatement ms = SqlParserHelper.getMappedStatement(metaObject); // 对应Mapper、dao中的方法 if("com.cn.tenant.dao.UserMapper.selectUserByPhone".equals(ms.getId())){ return true; } return false; }); ... }
上面这两种方式实现的功能相同,但是如果需要过滤的SQL语句很多,那么第二种方式配置起来会比较麻烦,因此建议通过注解的方式进行过滤。 除此之外,还有一个比较容易踩的坑就是在复制Bean时,不要复制租户id字段,否则会导致SQL语句报错:public void createSnapshot(Long userId){ User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getId, userId)); UserSnapshot userSnapshot=new UserSnapshot(); BeanUtil.copyProperties(user,userSnapshot); userSnapshotMapper.insert(userSnapshot); }
查看报错可以看出,本身Bean的租户字段不为空的情况下,SQL又自动添加一次租户查询条件,因此导致了报错: 我们可以修改复制Bean语句,手动忽略租户id字段,这里使用的是hutool的BeanUtil工具类,可以添加忽略字段。BeanUtil.copyProperties(user,userSnapshot,"tenantId");
在忽略了租户id的拷贝后,查询可以正常执行。 最后,再来看一下对联表查询的支持,首先看一下包含子查询的SQL:@Select("select * from user where id in (select id from user_snapshot)") List<User> selectSnapshot();
查看执行结果,可以看见,在子查询的内部也自动添加的租户查询条件: 再来看一下使用Join进行联表查询:@Select("select u.* from user u left join user_snapshot us on u.id=us.id") List<User> selectSnapshot();
同样,会在左右两张表上都添加租户的过滤条件: 再看一下不使用Join的普通联表查询:@Select("select u.* from user u ,user_snapshot us,dept d where u.id=us.id and d.id is not null") List<User> selectSnapshot();
查看执行结果,可以看见在这种情况下,只在FROM关键字后面的第一张表上添加了租户的过滤条件,因此如果使用这种查询方式,需要额外注意,用户需要手动在SQL语句中添加租户过滤。 Github上标星60K
的电商实战项目mall,全套 视频教程(2023最新版) 已更新完毕!全套教程约40小时,共113期
,通过这套教程你可以拥有一个涵盖主流Java技术栈的完整项目经验
,同时提高自己独立开发一个项目的能力
,下面是项目的整体架构图,感兴趣的小伙伴可以点击链接 mall视频教程 加入学习。 整套 视频教程 的内容还是非常完善的,涵盖了mall项目最佳学习路线、整体框架搭建、业务与技术实现全方位解析、线上Docker环境部署、微服务项目学习等内容,你也可以点击链接 mall视频教程 了解更多内容。推荐阅读 全面升级!一套基于Spring Boot 3+JDK17的实战项目! 69K Star!这是我见过最强的开源电商系统 !! Github标星60K!一套完整的项目实战教程来了,主流Java技术一网打尽! 看了我项目中购物车、订单、支付一整套设计,同事也开始悄悄模仿了... 支付系统就该这么设计,稳的一批! 权限系统就该这么设计,稳的一批! 上一篇: 工业网络搭建 “大神” Moxa EDS-408A,你了解吗? 下一篇: 从11s到170ms!看看人家的接口优化技巧,那叫一个优雅!