大家好,我是一安,今天介绍Mybatis-Plus一个多租户插件,可以实现同一套程序下用户数据的隔离。
前言
多租户是一种软件架构技术,在多用户的环境下,共有同一套系统,并且要注意数据之间的隔离性。
简单来说就是,在一台服务器上运行单个应用实例,它为多个租户(客户)提供服务,且保证用户间数据隔。
实现方案大致分三类:
-
独立数据库:即不同租户使用不同的数据库,隔离级别最高,安全性最好,但成本也相对最高 -
共享数据库、隔离数据架构:多租户使用同一个数据库,但是每个租户对应一个Schema -
共享数据库、共享数据架构:用同一个数据库和Schema,但是在表中增加了租户ID的字段,隔离级别最高,隔离级别最低,成本最低
具体实现
参考官网:https://baomidou.com/pages/aef2f2/#tenantlineinnerinterceptor
说明:
1.多租户 != 权限过滤,不要乱用,租户之间是完全隔离的!!!
2.启用多租户后所有执行的method的sql都会进行处理.
前两种,可通过请求头或请求参数携带租户ID实现动态切换,下面列出核心代码实现,具体实现可参考《实现多数据源切换(读写分离经典案例)》
1.根据这个类的 determineCurrentLookupKey()方法获取key对应的数据源,changeDataSource()实现切换数据源
public class DynamicDataSource extends AbstractRoutingDataSource {
private static final Logger LOGGER = LoggerFactory.getLogger(DynamicDataSource.class);
private static final ThreadLocal<String> DATA_SOURCE_KEY = new ThreadLocal<>();
static void changeDataSource(String dataSourceKey) {
DATA_SOURCE_KEY.set(dataSourceKey);
}
static void clearDataSource() {
DATA_SOURCE_KEY.remove();
}
@Override
protected Object determineCurrentLookupKey() {
String key = DATA_SOURCE_KEY.get();
LOGGER.info("current data-source is {}", key);
return key;
}
}
2.在请求进入Service层的时候根据注解动态的切换数据库
@Component
@Aspect
public class DataSourceConfig {
/*execution() 表达式的主体;
第一个”*“符号 表示返回值的类型任意;
com.sample.service.impl AOP所切的服务的包名,即,我们的业务部分
包名后面的”..“ 表示当前包及子包
第二个”*“ 表示类名,*即所有类。此处可以自定义,下文有举例
.*(..) 表示任何方法名,括号表示参数,两个点表示任何参数类型*/
//根据方法参数切换数据源
@Pointcut("execution(public * com.capitek.service.impl.*.*(..))")
public void pointCut() { }
@Before("pointCut()")
public void beforeSwitchDataSource(JoinPoint joinPoint) {
// 参数值
Object[] args = joinPoint.getArgs();
Arrays.asList(args).forEach(arg -> {
if(arg instanceof Map<?, ?>){
Map<String, Object> map = (Map<String, Object>) arg;
if(null!=map.get("city")){
if(map.get("city").equals("010")){
DynamicDataSource.changeDataSource(MultiDataSourceConfig.MASTER_DATA_SOURCE);
}else if(map.get("city").equals("029")){
DynamicDataSource.changeDataSource(MultiDataSourceConfig.SLAVE_DATA_SOURCE);
}else{
//默认使用北京库
DynamicDataSource.changeDataSource(MultiDataSourceConfig.MASTER_DATA_SOURCE);
}
}
}else if(arg instanceof JSONObject){
JSONObject param = (JSONObject) arg;
String city = param.getString("city");
if(null!=city){
if(city.equals("010")){
DynamicDataSource.changeDataSource(MultiDataSourceConfig.MASTER_DATA_SOURCE);
}else if(city.equals("029")){
DynamicDataSource.changeDataSource(MultiDataSourceConfig.SLAVE_DATA_SOURCE);
}else{
//默认使用北京库
DynamicDataSource.changeDataSource(MultiDataSourceConfig.MASTER_DATA_SOURCE);
}
}
}
});
}
@After("pointCut()")
public void afterSwitchDataSource() {
DynamicDataSource.clearDataSource();
}
下面我们来着重看一下第三种:
共享数据库,共享数据架构:因为这种方案服务器成本最低,但是提高了开发成本,好在Mybatis-plus提供了一种多租户的解决方案,实现方式是基于分页插件进行实现的,使用的Mybatis-plus版本为3.4.0
1.环境准备
CREATE TABLE user
(
id BIGINT(20) NOT NULL COMMENT '主键ID',
tenant_id BIGINT(20) NOT NULL COMMENT '租户ID',
name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
PRIMARY KEY (id)
);
DROP TABLE IF EXISTS user_addr;
CREATE TABLE USER_ADDR
(
id BIGINT(20) NOT NULL COMMENT '主键ID',
user_id BIGINT(20) NOT NULL COMMENT 'user.id',
name VARCHAR(30) NULL DEFAULT NULL COMMENT '地址名称',
PRIMARY KEY (id)
);
INSERT INTO user (id, tenant_id, name) VALUES
(1, 1, 'Jone'),(2, 1, 'Jack'),(3, 1, 'Tom'),
(4, 0, 'Sandy'),(5, 0, 'Billie');
INSERT INTO user_addr (id, USER_ID, name) VALUES
(1, 1, 'addr1'),(2,1,'addr2');
2.配置Mybatis
@Configuration
public class MybatisPlusConfig {
/**
* 新多租户插件配置,一缓和二缓遵循mybatis的规则,
* 需要设置 MybatisConfiguration#useDeprecatedExecutor = false
* 避免缓存出现问题(该属性会在旧插件移除后一同移除)
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
// 设置当前租户ID,实际可以从缓存,过滤器,请求参数,请求头都可以
@Override
public Expression getTenantId() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String tenant_id = request.getHeader("tenant_id");
return new LongValue(tenant_id);
}
//返回false表示该表需要拼多租户条件,true表示不需要
//实际可通过配置的方式引入
@Override
public boolean ignoreTable(String tableName) {
return "user_addr".equalsIgnoreCase(tableName);
}
}));
/** 如果用了分页插件注意先 add TenantLineInnerInterceptor
* 再 add PaginationInnerInterceptor
* 用了分页插件必须设置 MybatisConfiguration#useDeprecatedExecutor = false
*/
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
@Bean
public ConfigurationCustomizer configurationCustomizer() {
return configuration -> configuration.setUseDeprecatedExecutor(false);
}
/**
* 配置公共库数据源
* @return
* @throws Exception
*/
@Bean(name = "mysqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(@Qualifier("mysqlDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
//此处设置为了解决找不到mapper文件的问题
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/mysql/*.xml"));
//引入插件
sqlSessionFactoryBean.setPlugins(new Interceptor[]{
mybatisPlusInterceptor()
});
//map接收返回值值为null的问题,默认是当值为null,将key返回
MybatisConfiguration configuration = new MybatisConfiguration();
configuration.setCallSettersOnNulls(true);
sqlSessionFactoryBean.setConfiguration(configuration);
return sqlSessionFactoryBean.getObject();
}
}
3.实体类
/**
* 用户实体对应表 user
*/
@Data
public class User {
private Long id;
/**
* 租户 ID
*/
private Long tenantId;
private String name;
@TableField(exist = false)
private String addrName;
}
4.Mapper和xml
/**
* MP 支持不需要 UserMapper.xml
*/
public interface UserMapper extends BaseMapper<User> {
/**
* 自定义SQL:默认也会增加多租户条件
* 参考打印的SQL
* @return
*/
Integer myCount();
List<User> getAddrAndUser(@Param("name") String name);
List<User> getUserAndAddr(Page<User> pageParm,@Param("username") String username);
void insertUser(User user);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.test.mapper.UserMapper">
<select id="myCount" resultType="java.lang.Integer">
select count(1) from user
</select>
<select id="getUserAndAddr" resultType="com.test.entity.User">
select u.id, u.name, a.name as addr_name
from user u
left join user_addr a on a.user_id=u.id
<where>
<if test="username!=null">
u.name like concat(concat('%',#{username}),'%')
</if>
</where>
</select>
<select id="getAddrAndUser" resultType="com.test.entity.User">
select a.name as addr_name, u.id, u.name
from user_addr a
left join user u on u.id=a.user_id
<where>
<if test="name!=null">
a.name like concat(concat('%',#{name}),'%')
</if>
</where>
</select>
<insert id="insertUser">
insert into user (id,name) value (#{id},#{name})
</insert>
</mapper>
5.测试验证
@Autowired
private UserMapper userMapper;
@GetMapping("/get1")
public void dSelect() { // 查询
List<User> userList = userMapper.selectList(null);
userList.forEach(System.out::println);
}
@GetMapping("/get2")
public void manualSqlTenantFilterTest() {
System.out.println(userMapper.getAddrAndUser("addr1"));
Page<User> pageParm = new Page<>(1, 10);
System.out.println(userMapper.getUserAndAddr(pageParm,"Jone"));
User user = new User();
user.setId(6l);
user.setName("一安未来");
userMapper.insertUser(user);
}
2022-09-01 17:23:44.691 DEBUG 22200 --- [nio-8080-exec-4] c.t.mapper.UserMapper.getAddrAndUser : ==> Preparing: SELECT a.name AS addr_name, u.id, u.name FROM user_addr a LEFT JOIN user u ON u.id = a.user_id AND u.tenant_id = 1 WHERE a.name LIKE concat(concat('%', ?), '%')
2022-09-01 17:23:44.692 DEBUG 22200 --- [nio-8080-exec-4] c.t.mapper.UserMapper.getAddrAndUser : ==> Parameters: addr1(String)
2022-09-01 17:23:44.693 DEBUG 22200 --- [nio-8080-exec-4] c.t.mapper.UserMapper.getAddrAndUser : <== Total: 1
[User(id=1, tenantId=null, name=Jone, addrName=addr1)]
2022-09-01 17:23:44.697 DEBUG 22200 --- [nio-8080-exec-4] c.t.m.UserMapper.getUserAndAddr_mpCount : ==> Preparing: SELECT COUNT(1) FROM user u WHERE u.name LIKE concat(concat('%', ?), '%') AND u.tenant_id = 1
2022-09-01 17:23:44.697 DEBUG 22200 --- [nio-8080-exec-4] c.t.m.UserMapper.getUserAndAddr_mpCount : ==> Parameters: Jone(String)
2022-09-01 17:23:44.698 DEBUG 22200 --- [nio-8080-exec-4] c.t.m.UserMapper.getUserAndAddr_mpCount : <== Total: 1
2022-09-01 17:23:44.699 DEBUG 22200 --- [nio-8080-exec-4] c.t.mapper.UserMapper.getUserAndAddr : ==> Preparing: SELECT u.id, u.name, a.name AS addr_name FROM user u LEFT JOIN user_addr a ON a.user_id = u.id WHERE u.name LIKE concat(concat('%', ?), '%') AND u.tenant_id = 1 LIMIT ?
2022-09-01 17:23:44.700 DEBUG 22200 --- [nio-8080-exec-4] c.t.mapper.UserMapper.getUserAndAddr : ==> Parameters: Jone(String), 10(Long)
2022-09-01 17:23:44.702 DEBUG 22200 --- [nio-8080-exec-4] c.t.mapper.UserMapper.getUserAndAddr : <== Total: 2
[User(id=1, tenantId=null, name=Jone, addrName=addr1), User(id=1, tenantId=null, name=Jone, addrName=addr2)]
2022-09-01 17:59:37.009 DEBUG 21308 --- [nio-8080-exec-2] c.t.mapper.UserMapper.insertUser : ==> Preparing: INSERT INTO user (id, name, tenant_id) VALUES (?, ?, 1)
2022-09-01 17:59:37.010 DEBUG 21308 --- [nio-8080-exec-2] c.t.mapper.UserMapper.insertUser : ==> Parameters: 6(Long), 一安未来(String)
2022-09-01 17:59:37.035 DEBUG 21308 --- [nio-8080-exec-2] c.t.mapper.UserMapper.insertUser : <== Updates: 1
从打印sql语句可以看出user携带了租户ID,而user_addr并没有携带
插入的时候一定要注意不要携带租户ID,Mybatis-plus也会补齐
号外!号外!
如果这篇文章对你有所帮助,或者有所启发的话,帮忙点赞、在看、转发、收藏,你的支持就是我坚持下去的最大动力!
SpringBoot集成Kafka – 用Multi-Consumer实现数据高吞吐
原文始发于微信公众号(一安未来):基于Mybatis-Plus 3.4.0实现多租户方案
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/44890.html