在上一篇文章中,介绍了基于Mybatis-plus实现多租户架构,但是在实际工作中可能会存在一些不足:
- 如果是基于现有的系统改造,那么在所有的表上都需要加租户字段,会非常复杂
- 如果第三方系统调用我们的接口,可能无法要求他们在请求中携带租户信息,因此要重写大量的需要单独过滤的sql语句
针对上面的问题,接下来继续介绍一下基于Sharding-Sphere的分表来实现多租户,与之前在一张表中存放数据不同,我们会将不同租户的数据存放在同一数据库的不同表中,相对于前一种模式,这样会具有更高的数据隔离性。
首先来假设一个应用场景,某航空票务公司网站中,海航系、南航系和国航系被分为3个租户,租户间数据分表存放,它们下属的各个航空公司分别隶属于以上租户,那么随之各自的订单数据也存放在各自的租户数据表中。
首先先进行准备工作,为3个租户分别建表:
CREATE TABLE `t_order_0` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `order_number` varchar(32) DEFAULT NULL, `money` decimal(18,4) DEFAULT NULL, `postage` decimal(18,4) DEFAULT NULL, `address` varchar(128) DEFAULT NULL, `company` varchar(20) DEFAULT NULL, PRIMARY KEY (`id`) )
注意相同表结构的表需要建立三张,分别是t_order_0,t_order_1和t_order_2。
导入Sharding-Sphere依赖和数据库连接池druid的依赖:
<dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>sharding-jdbc-spring-boot-starter</artifactId> <version>4.1.1</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.22</version> </dependency>
注意这里引入的是druid而不是druid-spring-boot-starter,因为在高版本的sharding-sphere中,如果使用starter版本可能报错找不到url。
在application.yml中进行配置数据源及分表规则:
spring: shardingsphere: datasource: names: ds0 ds0: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/tenant?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC username: hydra password: 123456 sharding: defaultDataSourceName: ds0 tables: t_order: actualDataNodes: ds0.t_order_$->{0..2} tableStrategy: standard: shardingColumn: company preciseAlgorithmClassName: com.cn.hydra.shardingtest.algorithm.OrderShardingAlgorithm props: sql: show: true
对上面的参数进行说明:
datasource:这里因为还用不到分库,所以只进行了一个数据源的配置,如果存在多个则与ds0结构相同
defaultDataSourceName:选择默认数据源
tables:开始数据分片规则配置,注意下面的t_order是逻辑表名称
actualDataNodes :由数据源名加表名组成,以小数点分隔,多个表以逗号分隔,支持行表达式
tableStrategy:分表策略
standard:用于单分片键的标准分片场景
shardingColumn:分片列名称
preciseAlgorithmClassName:分片算法实现类,这个类由对我们自己实现,定义分片逻辑
props.sql.show:打印sql语句
创建一个枚举类,存放航空公司名称到租户id的对应关系,并写一个根据航空公司查找租户编码的方法,在后面分片规则中使用:
public enum Rules { NANHANG(0,Arrays.asList("NANFANG","XIAMEN","CHONGQING")), HAIHANG(1, Arrays.asList("SHOUDU","CHANGAN","JINPENG")), GUOHANG(2,Arrays.asList("GUOHANG","SHENZHEN","SHANHANG")); public static int searchCode(String company){ for (Rules value : Rules.values()) { if (value.getCompany().contains(company)){ return value.getCode(); } } return -1; } private int code; private List<String> company; Rules(int code,List<String> company){ this.code=code; this.company=company; } public int getCode() { return code; } public List<String> getCompany() { return company; } }
接下来是分表的核心,分片逻辑类需要实现PreciseShardingAlgorithm接口,并重写doSharding方法。之后对订单表的操作都会执行这里的doSharding方法选择实际执行sql的数据库表:
public class OrderShardingAlgorithm implements PreciseShardingAlgorithm<String> { @Override public String doSharding(Collection<String> collection, PreciseShardingValue<String> preciseShardingValue) { int tenant = Rules.searchCode(preciseShardingValue.getValue()); String targetTable="t_order_"+tenant; if (collection.contains(targetTable)){ return targetTable; } throw new UnsupportedOperationException("找不到租户:"+preciseShardingValue); } }
之前在yml中定义了分片列是company,因此这里通过preciseShardingValue能够拿到company的值。再根据上面枚举类的对应关系,可以获得租户id,最后返回真正执行sql的表名。
创建一个简单的订单Service进行测试,用来执行创建订单和查询订单的操作,参数都是航空公司的名称:
@Service public class OrderService { @Autowired OrderMapper orderMapper; public void createOrder(String company){ Order order=new Order(); order.setOrderNumber(UUID.randomUUID().toString().replaceAll("-","")); order.setMoney(new BigDecimal(100)); order.setCompany(company); orderMapper.insert(order); } public void getOrder(String company){ List<Order> orders = orderMapper.selectList(new LambdaQueryWrapper<Order>().eq(Order::getCompany, company)); orders.stream().forEach(System.out::println); } }
首先调用创建订单方法进行测试,发送一个请求:
http://127.0.0.1:8083/create?company=SHOUDU
查看执行结果的日志打印情况,被分为逻辑sql和实际执行的sql两部分。在逻辑sql语句中,可以看到使用的是逻辑表t_order,在实际sql中实际执行在t_order_1中,因为航空公司名称参数SHOUDU对应的租户编码是1,在分片算法中进行了实际表名的计算。
将参数换成SHENZHEN再执行一次:
http://127.0.0.1:8083/create?company=SHENZHEN
查看执行结果,实际执行的sql语句的表被换成了t_order_2:
接下来看一下可能发生的特殊情况,首先,如果传递的分片列的参数不在我们定义的映射规则内,那么会抛出UnsupportedOperationException异常:
如果在sql中没有涉及到分片列,那么数据会被插入到所有的表中,可以看到在下面的情况中,同一个订单被同时插入到了3张表中:
执行查询订单的方法进行测试:
http://127.0.0.1:8083/list?company=SHENZHEN
同样根据分片规则在表t_order_2中进行了实际的查询操作:
通过上面的实验,可以看出Sharding-Sphere的配置比较简单,在使用起来也是很方便的,通过客户端分片技术,能够很简单的实现基于分表的多租户需求。