大纲
1.单库版本到分库分表的演进介绍
2.订单系统项目模版
3.完成一次查询全过程
4.磁盘IO为什么这么慢
5.MySQL的索引是如何形成的
6.SQL优化
7.千万级数据优化之加缓存—理论
8.千万级数据优化之加缓存—实战
9.千万级数据优化之读写分离-理论
10.千万级数据优化之读写分离-实战
11.千万级数据优化-水平拆分和垂直拆分
12.分库分表多数据源实战
13.分库分表后订单路由策略和全局ID
14.MyCat和ShardingSphere介绍
15.创建库和表的小工具
16.ShardingSphere数据分片核心原理
17.C端ShardingSphere分片策略实现
18.C端ShardingSphere读写分离实现
19.异构订单实现
20.单库亿级数据怎么迁移到多库多表上
1.单库版本到分库分表的演进介绍
(1)单库版本的电商系统架构
(2)SQL优化阶段
(3)缓存优化阶段
(4)读写分离优化阶段
(5)垂直分库优化阶段
(6)分库分表版本的订单系统架构
2.订单系统项目模版
(1)创建订单的时序图
(2)查询订单列表的时序图
3.完成一次查询全过程
(1)用户发起查询请求到MySQL的流程
(2)MySQL收到查询语句的处理流程
(3)InnoDB引擎对查询语句的处理流程
4.磁盘IO为什么这么慢
(1)一次磁盘IO花费的时间
(2)一次内存IO花费的时间
(3)磁盘IO分为随机读写和顺序读写
(1)一次磁盘IO花费的时间
读写磁头在磁盘扇区上读取或者写入数据花费的时间,也就是一次完整的磁盘IO花费的时间,包括如下三个方面:
一.寻道时间
指的就是读写磁头移动到正确的半径上所需要的时间。寻道时间越短,磁盘IO操作越快。一般磁盘的寻道时间是3-15ms,主流的磁盘寻道时间是5ms。
二.旋转延迟时间
找到正确的磁道后,读写磁头移动到正确的位置上所消耗的时间。一般取磁盘旋转周期的一半作为旋转延迟的近似值:7200转/分 -> 120转/秒 -> 每转1/120秒 -> 每转的一半是1/240秒即4ms。
三.数据传输时间
指的是将数据从磁盘盘片读取或者写入的时间。一般是1ms以内,可以忽略不计。所以主流磁盘的一次磁盘IO的时间为:5ms + 4ms = 9ms。
(2)一次内存IO花费的时间
内存读取一次数据,一般是100ns以内,而1ms = 10^6ns = 100万ns。所以一次磁盘IO花费的时间是一次内存IO花费时间的约9万倍,几万倍。
(3)磁盘IO分为随机读写和顺序读写
一.顺序读写
就是读写磁头按照顺序读写磁盘盘片中的数据,速度还是很快的。MySQL里的binlog和redo日志就是顺序读写的。
二.随机读写
就是读写磁头会随机切换到不同的磁盘盘片的位置,速度比较缓慢耗时。
5.MySQL的索引是如何形成的
(1)数据页的结构
(2)数据区中存放的多个数据行组成单向链表
(3)多个数据页则组成双向链表
(4)索引页会存放数据页页号 + 其最小主键ID
(5)多个索引页的展示图
(1)数据页的结构
(2)数据区中存放的多个数据行组成单向链表
(3)多个数据页则组成双向链表
(4)索引页会存放数据页页号 + 其最小主键ID
(5)多个索引页的展示图
数据页和索引页组成的B+树中,叶子节点是数据页,非叶子节点是索引页。B+树的时间复杂度是log(n)。
6.SQL优化
(1)SQL优化流程
(2)SQL优化中的join优化原理算法
(1)SQL优化流程
判断join语句 -> 判断where条件 -> 判断聚合函数 -> 判断排序
(2)SQL优化中的join优化原理算法
SQL优化中不管是对where语句、聚合函数、还是排序操作的优化,优化起来相对而言会简单点,为对应的字段创建合适的索引即可。但是join语句这块的优化涉及到一些比较重要的如下原理了。
简单来说,在MySQL中使用join语句关联2张表的话,比如执行这条SQL:
select t1.order_no, t2.product_id
from order_info t1
left join order_item_detail t2
on t1.order_no = t2.order_no
这个时候,join关联查询的过程是什么样子的呢?其实,这个就取决于当前join语句用到的算法了,join语句一共有3种算法。
第一种:最基础的是Simple Nested Loop算法
简单嵌套循环算法,相当于双重for循环。
第二种:Block Nested Loop算法
MySQL提供了一个Join Buffer, 但是Join Buffer大小为256K,内存有限。当然我们也可以通过join buffer size参数调节Join Buffer的大小。
第三种:Index Nested Loop算法
原来的匹配次数为:驱动表行数 * 被驱动表行数,而现在变成了:驱动表行数 * 被驱动表索引的高度。这样就极大地减少了被驱动表的匹配次数,极大地提升了join的性能。
总结: 如果join关联查询能使用到索引,MySQL就会使用Index Nested Loop算法,查询效率会比较高。如果无法使用Index Nested Loop算法,MYSQL默认会使用Block Nested Loop算法,查询效率会很慢。
7.千万级数据优化之加缓存—理论
(1)高峰期导致数据库压力很大
(2)加缓存进行流量削峰
(3)如何提高缓存命中率
(1)高峰期导致数据库压力很大
通过对SQL优化特别是添加索引,可以提升查询的速度。此时已将SQL的一次查询稳定在300ms以下,但有一天DBA告知有一条SQL偶尔会超过2s。
为什么SQL查询时间会偶尔激增超过2s呢?通过排查发现,这条SQL在平时是没有问题的,一般稳定在300ms以下。经过分析和发现,在高峰期这条SQL才偶尔会超过2s。原因是在高峰期的时候,这台服务器的资源占用非常高。
所以定位到问题的原因是:高峰期时,大量请求会跑到MySQL数据库,从而导致这台服务器的CPU和内存占用率迅速飙升,最终导致数据库查询非常慢。
(2)加缓存进行流量削峰
解决方案:通过加缓存解决问题。
缓存的目的:为了流量削峰,减轻MySQL的负载,让MySQL稳定的去提供读写。
虽然说缓存非常好用,但是需要注意缓存命中率。缓存命中率 = 命中缓存的结果数 / 请求的缓存数。缓存命中率是衡量缓存有效性的重要指标。也就是说,命中率越高缓存的使用率越高。
(3)如何提高缓存命中率
第一:选择合适的业务场景
缓存适合读多写少的场景,最好是高频访问的场景,这里以已经完成订单为例。
第二:合理设置缓存容量
缓存容量如果很小则会触发Redis内存淘汰机制,导致key被删除,从而没能起到缓存效果。
一般都是二八原则,总数据量的20%会放在Redis里面,通常会根据业务场景取总数据量的15%到30%放到Redis中。
第三:控制缓存的粒度
单个key的数据单位越小,这个缓存就越不容容易更改,粒度越小缓存率越高。
第四:灵活设置缓存过期时间
如果缓存过期的时间设置不好,则可能会导致key同时失效。从而导致所有的请求都同时到数据库当中,这就是缓存击穿。
第五:避免缓存的穿透
查询一条数据,先从Redis缓存里查。如果没有,再从数据库中查,数据库中也没有,就说明缓存穿透。如果大量的请求过来,Redis中没有,数据库中也没有,就会把数据打穿。缓存穿透会造成缓存雪崩,导致最后服务崩溃。
解决办法:可以在缓存中给这个查询的请求设置一个空对象,让这个请求拿着空对象返回,然后设置到Redis缓存里。
第六:做好缓存预热
我们可以提前将数据库的数据(热点数据)放入到Redis缓存当中,这样第一次查询时就可以直接走缓存了。
下面是各个硬件的执行时间和容量:
CPU:20-50ns,1-32M级别
内存:100ns,32-96GB级别
磁盘:3-5ms,TB或PB级别
8.千万级数据优化之加缓存—实战
(1)业务场景是对历史订单进行查询
(2)Redis缓存的代码
(1)业务场景是对历史订单进行查询
由于历史订单的状态不会发生变化,符合读多写少的场景。所以可把用户的查询结果放入Redis缓存,并设置过期时间为1小时。只要缓存失效前再次查询,就会查Redis进行流量削峰来减轻数据库压力。
(2)Redis缓存的代码
缓存key的生成规则是:用户的ID+当前页+页数。
@RestController
@RequestMapping(value = "/user/order")
public class UserOrderController {
@Autowired
private UserOrderInfoService userOrderInfoService;
...
// 查询订单列表
@PostMapping("/queryOrderInfoList")
public PageResponse queryOrderInfoList(@RequestBody OrderInfoQuery orderInfoQuery) {
//开始计时
long bTime = System.currentTimeMillis();
try {
Page<OrderInfoVO> orderInfoPage = userOrderInfoService.queryOrderInfoList(orderInfoQuery);
//关闭分段计时
long eTime = System.currentTimeMillis();
//输出
log.info("查询用户订单耗时:" + (eTime - bTime));
return PageResponse.success(orderInfoPage);
} catch (Exception e) {
log.info(e.getMessage());
return PageResponse.error(e.getMessage());
}
}
...
}
//用户订单服务实现
@Service
public class UserOrderInfoServiceImpl implements UserOrderInfoService {
private final static Long FINISH = 50L;
@Autowired
private OrderInfoRepository orderInfoRepository;
@Autowired
private RedisUtils redisUtils;
...
@Override
public Page<OrderInfoVO> queryOrderInfoList(OrderInfoQuery orderInfoQuery) {
OrderValidation.checkVerifyOrderQuery(orderInfoQuery);
Page<OrderInfoVO> page = new Page<OrderInfoVO>();
page.setCurrent(orderInfoQuery.getCurrent());
page.setSize(orderInfoQuery.getSize());
//查询已完成的订单
if (FINISH.equals(orderInfoQuery.getOrderStatus())) {
//组装redisKey
String redisKey = orderInfoQuery.getUserId() + orderInfoQuery.getCurrent().toString() + orderInfoQuery.getSize().toString();
//获取redis缓存
Object redisObject = redisUtils.get(redisKey);
//redis为空则从数据库中查询
if (Objects.isNull(redisObject)) {
Page<OrderInfoVO> userOrderInfoVOPage = orderInfoRepository.queryOrderInfoList(page, orderInfoQuery);
//设置redis缓存,过期时间为一小时
redisUtils.set(redisKey, userOrderInfoVOPage, 3600L, TimeUnit.SECONDS);
return userOrderInfoVOPage;
}
log.info("从redis中获取数据, key: {}", redisKey);
return (Page<OrderInfoVO>) redisObject;
}
return orderInfoRepository.queryOrderInfoList(page, orderInfoQuery);
}
...
}
9.千万级数据优化之读写分离-理论
(1)读写分离的业务背景
(2)主从复制的原理是什么
(3)主从复制的几种模式
(1)读写分离的业务背景
营销系统那边做了一些活动,导致订单请求量突增,大量下单的用户可能会不断刷新订单来查询订单是否配送等信息。
此时大量的请求会打到MySQL上,而单库又抗不了这么多读请求。这就会导致数据库负载很高,从而严重降低MySQL的查询效率。现在我们缓存也加过了,但是数据库负载还是很高,此时该怎么办?
其实很简单,既然单个库扛不住,那就搞2个库一起来抗。因为这对于订单系统来说是典型的读多写少场景,所以在这个场景下可以搞个一主两从的架构来进行优化,就像如下这样:
也就是写数据走主库,而读数据走从库,并且多个从库可以一起来抗大量的读请求。关键的一点是,从库会通过主从复制,从主库中不断的同步数据,以此来保证从库的数据和主库一致。所以想要实现读写分离,那么就先要了解主从复制的具体原理。
(2)主从复制的原理是什么
我们以MySQL一主两从架构为例,也就是一个Master节点下有两个Slave节点。在这套架构下,写请求统一交给Master节点处理,而读请求交给Slave节点处理。
为了保证Slave节点和Master节点的数据一致性:Master节点在写入数据后,同时会把数据复制一份到自己的各个Slave节点上。
在复制过程中一共会使用到三个线程:一个是Binlog Dump线程,位于Master节点上。另外两个线程分别是IO线程和SQL线程,它们都分别位于Slave节点上。如下图示:
主从复制的核心流程:
步骤一: 首先,当Master节点接收到一个写请求时,这个写请求可能是增删改操作。此时会把写请求的操作都记录到BinLog日志中。
步骤二: 然后,Master节点会把数据复制给Slave节点,这个过程首先要每个Slave节点连接到Master节点上。当Slave节点连接到Master节点上时,Master节点会为每一个Slave节点分别创建一个BinLog Dump线程。每个BinLog Dump线程用于向各个Slave节点发送BinLog日志。
步骤三: BinLog Dump线程会读取Master节点上的BinLog日志,并将BinLog日志发送给Slave节点上的IO线程。
步骤四: Slave节点上的IO线程接收到BinLog日志后,会将BinLog日志先写入到本地的RelayLog中。Slave节点的RelayLog中就保存了Master的BinLog Dump线程发送过来的BinLog日志。
步骤五: 最后,Slave节点上的SQL线程就会来读取RelayLog中的BinLog日志,将其解析成具体的增删改操作。然后把这些在Master节点上进行过的操作,重新在Slave节点上也重做一遍,达到数据还原的效果。这样就可保证Master节点和Slave节点的数据一致性。
(3)主从复制的几种模式
MySQL的主从复制,分为全同步复制、异步复制、半同步复制和增强半同步复制这四种。
模式一:全同步复制
全同步复制就是当主库执行完一个事务后,所有从库也必须执行完该事务才可以返回结果给客户端。因此虽然全同步复制数据一致性得到保证,但主库完成一个事务需等待所有从库也完成,性能较低。
模式二:异步复制
异步复制就是当主库提交事务后,会通知BinLog Dump线程发送BinLog日志给从库。一旦BinLog Dump线程将BinLog日志发送给从库后,无需等从库也同步完事务,主库就会将处理结果返回给客户端。
因为主库只管自己执行完事务,就可以将处理结果返回给客户端,而不用关心从库是否执行完事务。这就可能导致短暂的主从数据不一致,比如刚在主库插入的新数据,如果马上在从库查询,就可能查询不到。
而且当主库提交事务后,如果宕机挂掉了,此时可能BinLog还没来得及同步给从库。这时如果为了恢复故障切换主从节点,就会出现数据丢失的问题。所以异步复制虽然性能高,但数据一致性上是较弱的。
不过MySQL主从复制,默认采用的就是异步复制这种复制策略。
模式三:半同步复制
半同步复制就是在同步和异步中做了折中选择,半同步主从复制的过程如下图示:
当主库提交事务后,至少还需要一个从库返回接收到BinLog日志,并成功写入到RelayLog的消息,这时主库才会将处理结果返回给客户端。相比前2种复制方式,半同步复制较好地兼顾了数据一致性以及性能损耗的问题。
但半同步复制也存在以下几个问题:
问题一: 半同步复制的性能相比异步复制有所下降。因为异步复制是不需要等待任何从库是否接收到数据的响应,而半同步复制则需要等待至少一个从库确认接收到binlog日志的响应,性能上是损耗更大。
问题二: 如果超过了配置的主库等待从库响应的最大时长,半同步复制就会变成异步复制,此时异步复制的问题同样会出现。
问题三: 在MySQL 5.7.2之前的版本中,半同步复制存在着幻读问题。当主库成功提交事务并处于等待从库确认过程,这时从库都还没来得及返回处理结果给客户端,但因为主库存储引擎内部已经提交事务了,所以其他客户端是可以到从主库中读到数据的。但是如果下一秒主库突然挂了,那么下一次请求过来时,就只能把请求切换到从库中。而因为从库还没从主库同步完数据,所以从库就读不到这条数据了。和上一秒读取数据的结果对比,就造成了幻读的现象。注意这不是数据丢失,因为后续从库会同步完数据的。
模式四:增强半同步复制
增强半同步复制是MySQL 5.7.2后的版本对半同步复制做的一个改进。原理上几乎是一样的,主要是解决幻读的问题。主库配置了参数rpl_semi_sync_master_wait_point = AFTER_SYNC后,主库在提交事务前必须先收到从库数据同步完成的确认信息,才能提交事务,以此来解决幻读问题。增强半同步主从复制过程如下:
10.千万级数据优化之读写分离-实战
(1)读写分离配置核心组件流程图
(2)读写分离的实现步骤
(1)读写分离配置核心组件流程图
(2)读写分离的实现步骤
步骤一:配置文件中配置主从库连接信息
application.yaml配置文件
spring:
datasource:
masters:
- url: jdbc:mysql://192.168.10.8:3307/order_db?autoReconnect=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT
username: root
password: Sharding@Master#1990
driver-class-name: com.mysql.cj.jdbc.Driver
slaves:
- url: jdbc:mysql://192.168.10.8:3308/order_db?autoReconnect=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT
username: root
password: Sharding@Slave#1990
driver-class-name: com.mysql.cj.jdbc.Driver
步骤二:注入数据源
DataSourceConfig类
//多数据源配置
@Configuration
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceConfig {
//主库数据源信息
private Map<String, String> masters;
//从库数据源信息
private List<Map<String, String>> slaves;
@Bean
public DataSource masterDataSource() throws Exception {
if (CollectionUtils.isEmpty(masters)) {
throw new Exception("主库数据源不能为空");
}
return DruidDataSourceFactory.createDataSource(masters);
}
@Bean
public List<DataSource> slaveDataSources() throws Exception {
if (CollectionUtils.isEmpty(slaves)) {
throw new Exception("从库数据源不能为空");
}
final List<DataSource> dataSources = new ArrayList<>();
for (Map map : slaves) {
dataSources.add(DruidDataSourceFactory.createDataSource(map));
}
return dataSources;
}
@Bean
@Primary
@DependsOn({"masterDataSource", "slaveDataSources"})
public DataSourceRouter routingDataSource() throws Exception {
final Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceContextHolder.MASTER, masterDataSource());
for (int i = 0; i < slaveDataSources().size(); i++) {
targetDataSources.put(DataSourceContextHolder.SLAVE + i, slaveDataSources().get(i));
}
final DataSourceRouter routingDataSource = new DataSourceRouter();
routingDataSource.setTargetDataSources(targetDataSources);
routingDataSource.setDefaultTargetDataSource(masterDataSource());
return routingDataSource;
}
//设置事务,事务需要知道当前使用的是哪个数据源才能进行事务处理
@Bean
public DataSourceTransactionManager dataSourceTransactionManager() throws Exception {
return new DataSourceTransactionManager(routingDataSource());
}
public Map<String, String> getMasters() {
return masters;
}
public void setMasters(Map<String, String> masters) {
this.masters = masters;
}
public List<Map<String, String>> getSlaves() {
return slaves;
}
public void setSlaves(List<Map<String, String>> slaves) {
this.slaves = slaves;
}
}
步骤三:数据源切换上下文,其中使用了ThreadLocal保存当前线程的数据源
DataSourceContextHolder类
//数据源上下文
public class DataSourceContextHolder {
public static final String MASTER = "MASTER";
public static final String SLAVE = "SLAVE";
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
//默认写模式
public static String getDataSourceType() {
return CONTEXT_HOLDER.get() == null ? MASTER : CONTEXT_HOLDER.get();
}
public static void setDataSourceType(String dataSourceType) {
if (dataSourceType == null) {
log.error("dataSource为空");
throw new NullPointerException();
}
log.info("设置dataSource:{}", dataSourceType);
CONTEXT_HOLDER.set(dataSourceType);
}
public static void removeDataSourceType() {
CONTEXT_HOLDER.remove();
}
}
步骤四:继承AbstractRoutingDataSource类重写determineCurrentLookupKey方法实现数据源动态切换
DataSourceRouter类继承了SpringBoot的AbstractRoutingDataSource类,并且重写了determineCurrentLookupKey方法来实现数据源动态切换。
//动态主从数据源切换
public class DataSourceRouter extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
log.info("当前数据源为" + DataSourceContextHolder.getDataSourceType());
//返回选择的数据源
return DataSourceContextHolder.getDataSourceType();
}
}
步骤五:创建读库的自定义注解
//自定义读库注解
//被这个注解的方法使用读库
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@Inherited
public @interface ReadOnly {
}
步骤六:切面类DynamicDataSourceAsepct
//数据源切面
@Aspect
@Component
public class DynamicDataSourceAspect implements Ordered {
//在Service层方法获取DataSource对象之前,在切面中指定当前线程数据源Slave
@Before(value = "execution(* *(..)) && @annotation(readOnly)")
public void before(JoinPoint point, ReadOnly readOnly) {
log.info(point.getSignature().getName() + "走从库");
DataSourceContextHolder.setDataSourceType(DataSourceContextHolder.SLAVE);
}
@After(value = "execution(* *(..)) && @annotation(readOnly)")
public void restoreDataSource(JoinPoint point, ReadOnly readOnly) {
log.info(point.getSignature().getName() + "清除数据源");
//方法执行完后清除数据源
DataSourceContextHolder.removeDataSourceType();
}
@Override
public int getOrder() {
return 0;
}
}
步骤七:在需要走读库的业务方法上添加@ReadOnly注解
这样执行这些业务方法时就会被切面拦截修改数据源,从而走读库进行查询。
//用户订单服务实现
@Service
public class UserOrderInfoServiceImpl implements UserOrderInfoService {
...
//获取订单详情
@ReadOnly
@Override
public OrderDetailVO getOrderDetail(String orderNo) {
return orderInfoRepository.getOrderDetail(orderNo);
}
...
}
11.千万级数据优化-水平拆分和垂直拆分
(1)什么时候考虑水平拆分和垂直拆分
(2)什么是垂直拆分
(3)什么是水平拆分
(1)什么时候考虑水平拆分和垂直拆分
当数据库的数据量越来越大时:首先可以从SQL入手进行优化,比如通过加入索引。然后可以使用缓存来进行优化,适合读多写少的场景。接着可以通过主从复制和读写分离来实现优化。此时增删改已全部走主库,查询都走从库,已经大大提升了读数据的能力。但是没有办法提升主库写数据能力,于是可以考虑对数据进行水平拆分和垂直拆分了。
(2)什么是垂直拆分
原来很多模块共用一个数据库资源,经过垂直分库后,商品模块、订单模块、用户模块等使用上自己单独的数据资源,于是各模块的资源竞争就不存在了。
垂直拆分的好处:
一.减轻了数据库的压力
二.每个数据库分摊数据,提高了查询的效率
三.每个数据库访问的CPU、内存、网络压力变小
四.业务更加清晰
五.解耦
六.系统扩展也变得容易
垂直拆分带来的不足:
一.系统的复杂性增加了
二.增加了多个数据库数据表联查的复杂性
三.事务处理变得麻烦
四.垂直拆分也解决不了单表数据量很大的问题
(3)什么是水平拆分
单表数据量很大可能会引起接口查询超时的问题。
履约系统会通过Dubbo发起RPC请求调用订单系统。假如设置的超时间为1s,此时订单模块的查询时间就已经快1s了,很容易超时。而一旦延长超时时间成5s,那么履约系统的的线程池就很容易被打满,导致资源迅速被耗尽,甚至会导致履约系统的服务雪崩。所以垂直拆分解决不了单表数据量很大的问题,需要水平拆分。
水平拆分时表的结构不会发生改变,水平拆分分为:水平拆表、水平分库、水平分库分表。
一.水平拆表如下
二.水平拆库如下
三.水平分库分表
水平拆分带来的不足:
一.水平拆分过程比较复杂
二.事务处理变得复杂
三.多库多表联查难度加大
四.水平拆分之后单表的数据会分散到不同的数据源中(多数据源管理问题)
12.分库分表多数据源实战
(1)首先配置好三个数据源
(2)然后创建一个数据库配置DataSourceConfig读入三个数据源的具体配置
(3)创建数据源上下文DataSourceContextHolder
(4)DataSourceRouter继承SpringBoot的AbstractRoutingDataSource实现动态切换数据源
(5)创建DataSources将数据源配置注入到DataSourceConfig对象中并交给SpringBoot管理
(6)在业务中设置当前线程的数据源类型
(1)首先配置好三个数据源
datasources.properties
spring.datasource01.url=jdbc:mysql://192.168.10.8:3307/order_db?autoReconnect=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT
spring.datasource01.username=root
spring.datasource01.password=Sharding@Master#1202
spring.datasource01.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource02.url=jdbc:mysql://192.168.10.8:3307/order_db?autoReconnect=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT
spring.datasource02.username=root
spring.datasource02.password=Sharding@Master#1202
spring.datasource02.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource03.url=jdbc:mysql://127.0.0.1/demo?autoReconnect=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT
spring.datasource03.username=root
spring.datasource03.password=123456
spring.datasource03.driver-class-name=com.mysql.cj.jdbc.Driver
(2)然后创建一个数据库配置DataSourceConfig读入三个数据源的具体配置
//数据库配置
@Data
public class DataSourceConfig {
//driverClassName驱动名称
private String driverClassName;
//url
private String url;
//用户名
private String username;
//密码
private String password;
}
(3)创建数据源上下文DataSourceContextHolder
写死各个数据源,并且为每个线程创建一个副本,当并发访问时,每个线程可以找到它对应的数据源。
//数据源上下文
public class DataSourceContextHolder {
public static final String DATA_SOURCE_01 = "datasource01";
public static final String DATA_SOURCE_02 = "datasource02";
public static final String DATA_SOURCE_03 = "datasource03";
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
public static void setDataSourceType(String dataSourceType) {
if (dataSourceType == null) {
log.error("dataSource为空");
throw new NullPointerException();
}
log.info("设置dataSource:{}", dataSourceType);
CONTEXT_HOLDER.set(dataSourceType);
}
public static String getDataSourceType() {
return CONTEXT_HOLDER.get();
}
//根据取模结果,设置当前线程的数据源类型
public static void setDataSourceType(Long userId) {
Long result = userId % DataSources.getDataSourceSize();
if (result == 0) {
DataSourceContextHolder.setDataSourceType(DataSourceContextHolder.DATA_SOURCE_01);
} else if (result == 1) {
DataSourceContextHolder.setDataSourceType(DataSourceContextHolder.DATA_SOURCE_02);
} else {
DataSourceContextHolder.setDataSourceType(DataSourceContextHolder.DATA_SOURCE_03);
}
}
public static void removeDataSourceType() {
CONTEXT_HOLDER.remove();
}
}
(4)DataSourceRouter继承SpringBoot的AbstractRoutingDataSource实现动态切换数据源
当创建了数据源上下文DataSourceContextHolder,每个线程是怎么找到它对应的数据源呢?
首先肯定需要在请求开始的时候通过DataSourceContextHolder的setDataSourceType()方法设置线程的数据源。由于DataSourceRouter继承SpringBoot的AbstractRoutingDataSource重写了determineCurrentLookupKey()方法,而在AbstractRoutingDataSource中SpringBoot已经实现了动态切换数据源的路由,所以每个线程便能借助SpringBoot去找到它对应的数据源了。
(5)创建DataSources将数据源配置注入到DataSourceConfig对象中并交给SpringBoot管理
//多数据源配置
@Configuration
@PropertySource("classpath:datasources.properties")
@ConfigurationProperties(prefix = "spring")
public class DataSources {
//数据源集合
private static List<DataSource> dataSources = new ArrayList<DataSource>();
//数据源01信息
private DataSourceConfig datasource01;
//数据源02信息
private DataSourceConfig datasource02;
//数据源03信息
private DataSourceConfig datasource03;
//获取数据源数量
public static int getDataSourceSize() {
return dataSources.size();
}
@Bean
public List<DataSource> ds() throws Exception {
dataSources.add(buildDruidDataSource(datasource01));
dataSources.add(buildDruidDataSource(datasource02));
dataSources.add(buildDruidDataSource(datasource03));
return dataSources;
}
//构建Druid数据源
public DruidDataSource buildDruidDataSource(DataSourceConfig dataSourceConfig) {
return DataSourceBuilder.create()
.type(DruidDataSource.class)
.driverClassName(dataSourceConfig.getDriverClassName())
.url(dataSourceConfig.getUrl())
.username(dataSourceConfig.getUsername())
.password(dataSourceConfig.getPassword())
.build();
}
@Bean
@Primary
public DataSourceRouter routingDataSource() throws Exception {
Map<Object, Object> targetDataSources = new HashMap<>();
List<DataSource> dataSources = ds();
targetDataSources.put(DataSourceContextHolder.DATA_SOURCE_01, dataSources.get(0));
targetDataSources.put(DataSourceContextHolder.DATA_SOURCE_02, dataSources.get(1));
targetDataSources.put(DataSourceContextHolder.DATA_SOURCE_03, dataSources.get(2));
DataSourceRouter routingDataSource = new DataSourceRouter();
routingDataSource.setTargetDataSources(targetDataSources);
routingDataSource.setDefaultTargetDataSource(dataSources.get(0));
return routingDataSource;
}
//设置事务,事务需要知道当前使用的是哪个数据源才能进行事务处理
@Bean
public DataSourceTransactionManager dataSourceTransactionManager() throws Exception {
return new DataSourceTransactionManager(routingDataSource());
}
public DataSourceConfig getDatasource01() {
return datasource01;
}
public void setDatasource01(DataSourceConfig datasource01) {
this.datasource01 = datasource01;
}
public DataSourceConfig getDatasource02() {
return datasource02;
}
public void setDatasource02(DataSourceConfig datasource02) {
this.datasource02 = datasource02;
}
public DataSourceConfig getDatasource03() {
return datasource03;
}
public void setDatasource03(DataSourceConfig datasource03) {
this.datasource03 = datasource03;
}
}
(6)在业务中设置当前线程的数据源类型
@Repository
public class OrderInfoRepository {
...
//查询用户订单列表
public Page<OrderInfoVO> queryOrderInfoList(OrderInfoBaseQuery orderInfoBaseQuery) {
//1.组装mybatis查询插件
Page page = new Page();
page.setCurrent(orderInfoBaseQuery.getPageNo());
page.setSize(orderInfoBaseQuery.getPageSize());
//2.设置当前线程的数据源类型
DataSourceContextHolder.setDataSourceType(orderInfoBaseQuery.getUserId());
//3.查询该用户订单信息
Page<OrderInfoVO> orderInfoVOPage = orderInfoMapper.queryOrderInfoList(page, orderInfoBaseQuery);
return orderInfoVOPage;
}
...
}
13.分库分表后订单路由策略和全局ID
(1)为什么需要路由策略
(2)路由策略的设计
(3)全局ID的作用
(4)全局唯一ID的算法之雪花算法
(1)为什么需要路由策略
一.分库分表后必须要支持的场景
场景一:用户下单后需要查看订单信息了解订单状态
场景二:商家需要获取订单信息进行分析
二.分库分表后不同数据会分散在不同的库表
一个用户下单后,订单可能会落在不同的表中,他查询自己的订单列表时可能需要查多张表。同样,商家查询他的订单列表时也可能需要查多张表。
所以希望根据一定规则,让同一个用户下的单在同一张表,同一个商家的单在同一张表,这个规则就是路由策略。
(2)路由策略的设计
一.用户的路由策略之使用用户ID作为路由Key
单库多表的路由策略:用户ID的Hash值 % 3
用户下单时,把用户ID作为路由Key:
首先对用户ID取Hash值,然后对表的数量进行取模。
多库多表的路由策略:需要先找到库再找到表
找库:根据用户ID的Hash值对数据库的数量进行取模找到对应的数据库;
找表:根据用户ID的Hash值除以数据库的数量接着再对表的数量进行取模找到对应的表;
可以用2库3表来简单举1-10的ID例子分析理解
找库:用户ID的Hash值 % 2;
找表:用户ID的Hash值 / 2 % 3;
设计路由策略的要点:根据具体的业务场景、与用户信息关联度比较大的路由Key进行Hash值取模。
二.商家的路由策略之使用商家ID作为路由Key并单独为商家B端设计一张订单表
很明显不能使用用户ID作为商家的路由Key。我们会单独为商家B端设计了一张订单表,也就是C端和B端的订单表是相互独立的。
用户的角度查询订单,是以用户ID作为路由Key。商家的角度查询订单,是以商家ID作为路由Key。B端订单表的路由策略和C端订单表的路由策略是一致的,仅仅是路由Key不同。
那么如何为B端的订单表写入数据呢?当C端用户下单时,会把订单号发送到MQ。然后B端会消费这个MQ消息,根据订单号获取订单信息,然后写入到B端的订单表中。
(3)全局ID的作用
全局ID是为了解决订单主键重复的问题。可以引入一个全局唯一ID的服务,用来生成全局唯一ID。该服务每次生成的ID都不一样,可以保证主键的唯一性。
(4)全局唯一ID的算法之雪花算法
雪花算法生成的ID一共64个bit位 = 8字节。
第一部分:0表示正数。
第二部分:41位的时间戳,2^41 - 1 = 69年。
第三部分:10位的全局机器ID,其中5位的数据中心 + 5位机器ID。2 ^10 = 1024,表示在同一个时间戳下,最多可允许1024台机器得到全局唯一ID。
第四部分:12位的序列号 2^12 = 4096。
雪花算法的含义:
就是同一个时间戳下,可为1024台机器获取全局唯一ID,可为每台机器并发分配4096个ID。
雪花算法的ID生成类:
public class IdGenerater {
//数据中心id:外部传入
private long datacenterId;
//机器id:外部传入
private long workerId;
//12位的序列号
private long sequence;
//数据中心id的bit位数
private long datacenterIdBits = 5L;
//工作id的bit位数
private long workerIdBits = 5L;
//序列号bit位数
private long sequenceBits = 12L;
//工作id向左移动位数:12位
private long workerIdShift = sequenceBits;
//数据id向左移动位数:12 + 5 = 17位
private long datacenterIdShift = sequenceBits + workerIdBits;
//时间戳需要左移位数 12 + 5 + 5 = 22位
private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
//序列号最大值
private long sequenceMask = -1L ^ (-1L << sequenceBits);
//初始时间戳
private long initialTimestamp = 1288834974657L;
//上次时间戳,初始值为负数是不想浪费 lastTimestamp + 1 = 0 这个序列号
private long lastTimestamp = -1L;
public IdGenerater(long workerId, long datacenterId, long sequence) {
this.workerId = workerId;
this.datacenterId = datacenterId;
this.sequence = sequence;
}
//通过雪花算法生成一个ID
public synchronized long nextId() throws Exception {
long timestamp = System.currentTimeMillis();
//获取当前时间戳:当前时间戳不能小于上次的获取的时间戳
if (timestamp < lastTimestamp) {
throw new RuntimeException("当前时间戳小于上次的时间戳,可能时钟回拨了!");
}
//当前时间戳,如果等于上次时间戳(同一毫秒内),则序列号加一
//否则序列号赋值为0,从0开始
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & sequenceMask;
//序列号超过12个bit位、也就是已经生成4096个ID了
//那这一毫秒内就不能再生成新的ID了,切换到下一毫秒内
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
//第一个ID的序列号,从0开始
sequence = 0;
}
//将当前时间戳设置为最近的一次时间戳
lastTimestamp = timestamp;
//返回结果:
//(timestamp - initialTimestamp) << timestampLeftShift) 表示将时间戳减去初始时间戳,再左移相应位数
//(datacenterId << datacenterIdShift) 表示将数据id左移相应位数
//(workerId << workerIdShift) 表示将工作id左移相应位数
return ((timestamp - initialTimestamp) << timestampLeftShift) |
(datacenterId << datacenterIdShift) |
(workerId << workerIdShift) | sequence;
}
//获取时间戳,并与上次时间戳比较
private long tilNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}
雪花算法的测试类:
//测试雪花算法生成ID
public class SnowFlakeTest {
public static void main(String[] args) throws Exception {
IdGenerater worker = new IdGenerater(1, 1, 1);
for (int i = 0; i < 5; i++) {
long id = worker.nextId();
System.out.println("id为:" + id);
System.out.println("二进制id为:" + Long.toBinaryString(id));
}
}
}
需要注意雪花算法的时钟回拨问题:也就是存在的人为调整系统时间,导致生成同样的ID。
雪花算法的时钟回拨问题的解决方案:可以把上一次系统获取到的时间戳缓存起来,每次获取时间戳和上次的进行比较。如果小于上一次,就证明时钟回拨,这时候就可以取上次时间戳 + 1即可。
14.MyCat和ShardingSphere介绍
(1)MyCat简介
(2)ShardingSphere简介
(1)MyCat简介
如果没有MyCat,系统需要自己管理数据库的配置。
如果有MyCat,数据库的配置会由MyCat管理。同时MyCat可以帮我们完成:读写分离、数据分片路由、多数据源管理等功能。
具体的流程如下:
MyCat适用于中小型公司、创业公司,MyCat是需要单独部署一个服务。如果数据库特别多,则MyCat的压力会变得很大,从而导致性能降低。
(2)ShardingSphere简介
和MyCat不一样的是,ShardingSphere只是一个jar包,更加轻量级。只需要在项目中引入即可,无需独立部署服务。使用ShardingSphere时的系统和MySQL交互流程如下:
ShardingSphere是一款分布式的数据库生态系统,它可以将任意数据库转换为分布式数据库,它还可以通过数据分片、弹性伸缩、加密等能力对原有数据库增强。
ShardingSphere主要由ShardingSphere-JDBC和ShardingSphere-Proxy组成。ShardingSphere-JDBC定位为轻量级Java框架,在Java的JDBC层提供额外服务。ShardingSphere-Proxy定位为透明化的数据库代理端,通过实现数据库二进制协议,对异构语言提供支持。ShardingSphere-JDBC是基于已经有的库和表进行操作的,会进行数据分片(sharding)和SQL解析(parse)。
ShardingSphere的功能有:数据分片、分布式事务、读写分离、高可用、数据库网关、流量治理、数据迁移、数据加密、数据脱密。
15.创建库和表的小工具
(1)简单总结
(2)创建库和表的工具
(1)简单总结
从项目整体演进的角度:单体项目 -> 流量暴增 -> 查询变慢 -> SQL优化 + 索引 -> 引入缓存 -> 读写分离 -> 垂直拆分 + 水平拆分 -> 分库分表所需要支持的场景(B端和C端) -> 路由设计策略 -> 全局ID(分布式ID)的雪花算法 -> ShardingSphere引入
从数据流动方向的角度:请求数据 -> 业务逻辑处理 -> Mapper生成逻辑SQL -> 选择路由Key -> 选择路由策略 -> 选择具体的数据库 -> 选定具体的表 -> ShardingSphere生成实际的SQL
ShardingSphere并不会帮助我们先把库和表生成好,ShardingSphere是基于已经有的库和表进行操作的,会进行数据分片(sharding)和SQL解析(parse)。所以使用ShardingSphere之前,需要先创建好库和表。
(2)创建库和表的工具
步骤一:配置好分片的策略,两个数据源、8库8表、两个维度的表
也就是8个库会落在两个数据源上,每个库有8张用户维度的表和8张商家维度的表。
shardingstrategyConfig.properties
#数据源的配置,多个数据源参考下边的配置
#数据源1
shardingstrategy.datasources[0].hostName=192.168.10.8
shardingstrategy.datasources[0].port=3307
shardingstrategy.datasources[0].username=root
shardingstrategy.datasources[0].password=Sharding@Master#1990
#数据源2
#shardingstrategy.datasources[1].hostName=xxx.xxx.xx.xxx
#shardingstrategy.datasources[1].port=3306
#shardingstrategy.datasources[1].username=root
#shardingstrategy.datasources[1].password=123456
#分库的配置
#分库分表前的库名
shardingstrategy.originDbName=order_db
#要分的逻辑库总数
shardingstrategy.dbNum=8
#库名的后缀
shardingstrategy.dbSuffix=_
#分表的配置
#每个逻辑库中表的数量
shardingstrategy.tableNumPerDb=8
#表名后缀,一种分表分库维度对应一种后缀,比如C端维度:_by_user_id_ 商户端维度:_by_merchant_id_
shardingstrategy.tableSuffix=_sharded_by_user_id_,_sharded_by_merchant_id_
步骤二:加载配置类并交给Spring管理
将配置类shardingstrategyConfig.properties的信息加载到ShardingStrategyConfig类中。
@Data
@Component
@ConfigurationProperties(prefix = "shardingstrategy")
@PropertySource("classpath:shardingstrategyConfig.properties")
public class ShardingStrategyConfig {
//原始库名,即分库分表前的库名
private String originDbName;
//要分的逻辑库总数
private int dbNum;
//每个逻辑库中表的数量
private int tableNumPerDb;
//库名的后缀
private String dbSuffix;
//表名后缀,一种分表分库维度对应一种后缀,比如C端维度:_by_user_id_ 商户端维度:_by_merchant_id_
private List<String> tableSuffix;
//数据源
private List<DataSourceConfig> datasources;
}
@Data
public class DataSourceConfig {
//主机名
private String hostName;
//端口号
private int port;
//用户名
private String username;
//密码
private String password;
}
步骤三:创建ShardingUtil类生成具体的库和表
@Component
public class ShardingUtil {
private static final Logger logger = LoggerFactory.getLogger(ShardingUtil.class);
@Autowired
private ShardingStrategyConfig shardingStrategyConfig;
//创建库和表
public void initTables(String sqlPath) {
//1.校验sqlPath
if (StringUtils.isBlank(sqlPath)) {
throw new IllegalArgumentException("sqlPath cannot be empty");
}
Optional.of(shardingStrategyConfig)
.map(ShardingStrategyConfig::getDatasources)
.ifPresent(dataSources -> {
//逻辑库的索引
AtomicInteger dbIndex = new AtomicInteger();
//2.依次处理每个数据源(物理库)
dataSources.stream().filter(Objects::nonNull).forEach(dataSourceConfig -> {
//3.创建数据源,建库用临时DataSource
MysqlDataSource dataSource = buildDataSource(dataSourceConfig);
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
//4.获取配置的数据库名称
String originDbName = shardingStrategyConfig.getOriginDbName();
logger.info("originDbName:[{}],prepare to process the data source:[{}]", originDbName, dataSource.getUrl());
//5.每个数据源需要创建多少个逻辑库 逻辑库总数/物理库总数 = 每个物理库上应该创建多少个逻辑库
int dbNumPerDataSource = shardingStrategyConfig.getDbNum() / dataSources.size();
//6.依次创建逻辑库
for (int i = 0; i < dbNumPerDataSource; i++) {
//7.创建逻辑数据库
String targetDbName = createDatabase(dbIndex, jdbcTemplate, originDbName);
//8.在指定的逻辑库中创建table
createTables(sqlPath, dataSource, targetDbName);
}
});
});
}
//在指定的逻辑db中创建table
private void createTables(String sqlPath, MysqlDataSource dataSource, String targetDbName) {
//1.指定建表的逻辑库
dataSource.setDatabaseName(targetDbName);
//2.创建建表使用的jdbcTemplate
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
try {
//3.拼接建表sql
String finalTableSql = splicingFinalTableSql(sqlPath);
//4.执行建表sql
jdbcTemplate.execute(finalTableSql);
logger.info("DB[{}] create table complete...", targetDbName);
} catch (Exception e) {
logger.error("createTables fail,e:[{}]", e.getMessage());
}
}
//创建逻辑库
private String createDatabase(AtomicInteger dbIndex, JdbcTemplate jdbcTemplate, String originDbName) {
//1.拼接要创建的目标数据库名称
String targetDbName = originDbName + shardingStrategyConfig.getDbSuffix() + (dbIndex.getAndIncrement());
//2.创建逻辑库
jdbcTemplate.execute(String.format("CREATE DATABASE IF NOT EXISTS `%s` DEFAULT CHARACTER SET UTF8", targetDbName));
logger.info("DB[{}]create db complete...", targetDbName);
return targetDbName;
}
//拼接建表sql
private String splicingFinalTableSql(String sqlPath) throws IOException {
StringBuilder finalSqlBuilder = new StringBuilder();
//1.使用流读取sqlPath文件中配置的sql语句
InputStream inputStream = ShardingUtil.class.getResourceAsStream(sqlPath);
if (Objects.isNull(inputStream)) {
throw new IOException("The specified sqlPath file does not exist");
}
//2.将流转换成字符串
String sqlText = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
for (int i = 0; i < shardingStrategyConfig.getTableNumPerDb(); i++) {
int tableIndex = i;
//3.依次处理每条sql
Optional.of(sqlText.split(";"))
.ifPresent(sqls -> Arrays.stream(sqls)
.filter(Objects::nonNull)
.forEach(sql -> {
//4.依次处理配置的表后缀,拼接出相应后缀名的sql
shardingStrategyConfig.getTableSuffix().forEach(tableSuffix -> {
//5.为sql中的表名拼接后缀
String finalTableSql = splicingTableSuffix(sql, tableSuffix + tableIndex);
//6.将最终要执行的sql统一拼接起来
finalSqlBuilder.append(finalTableSql);
});
}));
}
return finalSqlBuilder.toString();
}
//为sql中的表名拼接后缀
private String splicingTableSuffix(String sql, String suffix) {
//1.编写匹配表名的正则表达式,(?i)表示忽略大小写,(?s)表示开启单行模式,即多行sql就像在同一行一样,方便匹配
Pattern tablePattern = Pattern.compile("(?i)(?s)CREATE\s+TABLE\s+(\S+)");
StringBuilder sb = new StringBuilder(sql);
//2.使用正则表达式来匹配表名
Matcher matcher = tablePattern.matcher(sb);
if (matcher.find()) {
//3.在表名后边,拼接上指定的后缀名
sb.insert(matcher.end(), suffix)
.append(";")
.append("\n\n");
logger.debug("match to table:[{}]", matcher.group(1));
return sb.toString();
}
return "";
}
//创建数据源
private MysqlDataSource buildDataSource(DataSourceConfig dataSourceConfig) {
MysqlDataSource dataSource = new MysqlDataSource();
dataSource.setServerName(dataSourceConfig.getHostName());
dataSource.setPort(dataSourceConfig.getPort());
dataSource.setUser(dataSourceConfig.getUsername());
dataSource.setPassword(dataSourceConfig.getPassword());
dataSource.setCharacterEncoding("utf-8");
//开启批处理
dataSource.setAllowMultiQueries(Boolean.TRUE);
return dataSource;
}
}
步骤四:通过测试类执行创建库和表
@SpringBootTest
@RunWith(SpringRunner.class)
public class ShardingUtilTest {
@Autowired
private ShardingUtil shardingUtil;
@Test
public void test_initTables() {
shardingUtil.initTables("/sql/init.sql");
}
}
16.ShardingSphere数据分片核心原理
(1)ShardingSphere数据分片的流程
(2)ShardingSphere订单系统流程
由于这里使用的是版本4系列作为例子,所以可以参考下面的官网。
https://shardingsphere.apache.org/document/4.1.1/cn/overview/
(1)ShardingSphere数据分片的流程
一.SQL解析
分为词法解析和语法解析。先通过词法解析器将SQL拆分为一个个不可再分的单词,再使用语法解析器对SQL进行理解,并最终提炼出解析上下文。解析上下文包括表、选择项、排序项、分组项、聚合函数、分页信息、查询条件以及可能需要修改的占位符的标记。执行器优化合并和优化分片条件,如OR等。
二.SQL路由
根据解析上下文匹配用户配置的分片策略,并生成路由路径。目前支持分片路由和广播路由。
三.SQL改写
将SQL改写为在真实数据库中可以正确执行的语句,SQL改写分为正确性改写和优化改写。
四.SQL执行
通过多线程执行器异步执行。
五.结果归并
将多个执行结果集归并以便于通过统一的JDBC接口输出,结果归并包括流式归并、内存归并和使用装饰者模式的追加归并这几种方式。
(2)ShardingSphere订单系统流程
17.C端ShardingSphere分片策略实现
(1)在application.properties进行分库分表配置
(2)编写C端维度分库路由算法类
(3)编写C端维度分表路由算法类
(4)创建订单的具体实现
(1)在application.properties进行分库分表配置
以下是基于Spring Boot的规则配置:配置了8主8从,8个主库在数据源1—192.168.10.8:3307,8个从库在数据源2—192.168.10.8:3308。
//参考:https://shardingsphere.apache.org/document/4.1.1/cn/manual/sharding-jdbc/usage/sharding/
#分库分表配置
#打印执行的数据库以及语句
spring.shardingsphere.props.sql.show=true
spring.shardingsphere.datasource.names=master0,slave0,master1,slave1,master2,slave2,master3,slave3,master4,slave4,master5,slave5,master6,slave6,master7,slave7
spring.shardingsphere.datasource.master0.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.master0.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.master0.url=jdbc:mysql://192.168.10.8:3307/order_db_0?characterEncoding=utf-8
spring.shardingsphere.datasource.master0.username=root
spring.shardingsphere.datasource.master0.password=Sharding@Master#1990
spring.shardingsphere.datasource.slave0.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.slave0.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.slave0.url=jdbc:mysql://192.168.10.8:3308/order_db_0?characterEncoding=utf-8
spring.shardingsphere.datasource.slave0.username=root
spring.shardingsphere.datasource.slave0.password=Sharding@Slave#1990
spring.shardingsphere.datasource.master1.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.master1.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.master1.url=jdbc:mysql://192.168.10.8:3307/order_db_1?characterEncoding=utf-8
spring.shardingsphere.datasource.master1.username=root
spring.shardingsphere.datasource.master1.password=Sharding@Master#1990
spring.shardingsphere.datasource.slave1.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.slave1.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.slave1.url=jdbc:mysql://192.168.10.8:3308/order_db_1?characterEncoding=utf-8
spring.shardingsphere.datasource.slave1.username=root
spring.shardingsphere.datasource.slave1.password=Sharding@Slave#1990
spring.shardingsphere.datasource.master2.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.master2.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.master2.url=jdbc:mysql://192.168.10.8:3307/order_db_2?characterEncoding=utf-8
spring.shardingsphere.datasource.master2.username=root
spring.shardingsphere.datasource.master2.password=Sharding@Master#1990
spring.shardingsphere.datasource.slave2.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.slave2.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.slave2.url=jdbc:mysql://192.168.10.8:3308/order_db_2?characterEncoding=utf-8
spring.shardingsphere.datasource.slave2.username=root
spring.shardingsphere.datasource.slave2.password=Sharding@Slave#1990
spring.shardingsphere.datasource.master3.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.master3.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.master3.url=jdbc:mysql://192.168.10.8:3307/order_db_3?characterEncoding=utf-8
spring.shardingsphere.datasource.master3.username=root
spring.shardingsphere.datasource.master3.password=Sharding@Master#1990
spring.shardingsphere.datasource.slave3.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.slave3.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.slave3.url=jdbc:mysql://192.168.10.8:3308/order_db_3?characterEncoding=utf-8
spring.shardingsphere.datasource.slave3.username=root
spring.shardingsphere.datasource.slave3.password=Sharding@Slave#1990
spring.shardingsphere.datasource.master4.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.master4.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.master4.url=jdbc:mysql://192.168.10.8:3307/order_db_4?characterEncoding=utf-8
spring.shardingsphere.datasource.master4.username=root
spring.shardingsphere.datasource.master4.password=Sharding@Master#1990
spring.shardingsphere.datasource.slave4.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.slave4.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.slave4.url=jdbc:mysql://192.168.10.8:3308/order_db_4?characterEncoding=utf-8
spring.shardingsphere.datasource.slave4.username=root
spring.shardingsphere.datasource.slave4.password=Sharding@Slave#1990
spring.shardingsphere.datasource.master5.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.master5.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.master5.url=jdbc:mysql://192.168.10.8:3307/order_db_5?characterEncoding=utf-8
spring.shardingsphere.datasource.master5.username=root
spring.shardingsphere.datasource.master5.password=Sharding@Master#1990
spring.shardingsphere.datasource.slave5.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.slave5.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.slave5.url=jdbc:mysql://192.168.10.8:3308/order_db_5?characterEncoding=utf-8
spring.shardingsphere.datasource.slave5.username=root
spring.shardingsphere.datasource.slave5.password=Sharding@Slave#1990
spring.shardingsphere.datasource.master6.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.master6.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.master6.url=jdbc:mysql://192.168.10.8:3307/order_db_6?characterEncoding=utf-8
spring.shardingsphere.datasource.master6.username=root
spring.shardingsphere.datasource.master6.password=Sharding@Master#1990
spring.shardingsphere.datasource.slave6.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.slave6.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.slave6.url=jdbc:mysql://192.168.10.8:3308/order_db_6?characterEncoding=utf-8
spring.shardingsphere.datasource.slave6.username=root
spring.shardingsphere.datasource.slave6.password=Sharding@Slave#1990
spring.shardingsphere.datasource.master7.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.master7.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.master7.url=jdbc:mysql://192.168.10.8:3307/order_db_7?characterEncoding=utf-8
spring.shardingsphere.datasource.master7.username=root
spring.shardingsphere.datasource.master7.password=Sharding@Master#1990
spring.shardingsphere.datasource.slave7.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.slave7.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.slave7.url=jdbc:mysql://192.168.10.8:3308/order_db_7?characterEncoding=utf-8
spring.shardingsphere.datasource.slave7.username=root
spring.shardingsphere.datasource.slave7.password=Sharding@Slave#1990
#用于多分片键的复合分片场景
#C端order_info分表策略分片策略
#分片列名称,多个列以逗号分隔
spring.shardingsphere.sharding.tables.order_info_sharded_by_user_id_.database-strategy.complex.sharding-columns=order_no,user_id
#复合分片算法类名称,该类需实现ComplexKeysShardingAlgorithm接口并提供无参数的构造器
spring.shardingsphere.sharding.tables.order_info_sharded_by_user_id_.database-strategy.complex.algorithm-class-name=com.demo.sharding.order.sharding.algorithm.OrderDbShardingByUserAlgorithm
#C端分表策略
spring.shardingsphere.sharding.tables.order_info_sharded_by_user_id_.actual-data-nodes=master$->{0..7}.order_info_sharded_by_user_id_$->{0..7}
#分片字段
spring.shardingsphere.sharding.tables.order_info_sharded_by_user_id_.table-strategy.complex.sharding-columns=order_no,user_id
#order_info表分片策略类
spring.shardingsphere.sharding.tables.order_info_sharded_by_user_id_.table-strategy.complex.algorithm-class-name=com.demo.sharding.order.sharding.algorithm.OrderTableShardingByUserAlgorithm
#主键名称,缺省表示不使用自增主键生成器
spring.shardingsphere.sharding.tables.order_info_sharded_by_user_id_.key-generator.column=id
#主键生成器类型,内置类型:SNOWFLAKE/UUID
spring.shardingsphere.sharding.tables.order_info_sharded_by_user_id_.key-generator.type=SNOWFLAKE
#工作机器 id
spring.shardingsphere.sharding.tables.order_info_sharded_by_user_id_.key-generator.props.worker.id=${workerId}
#时钟回拨可以容忍的毫秒数,默认10ms
spring.shardingsphere.sharding.tables.order_info_sharded_by_user_id_.key-generator.props.max.tolerate.time.difference.milliseconds=5
#C端order_item_detail分表策略分片策略
#分片列名称,多个列以逗号分隔
spring.shardingsphere.sharding.tables.order_item_detail_sharded_by_user_id_.database-strategy.complex.sharding-columns=order_no,user_id
#复合分片算法类名称,该类需实现ComplexKeysShardingAlgorithm接口并提供无参数的构造器
spring.shardingsphere.sharding.tables.order_item_detail_sharded_by_user_id_.database-strategy.complex.algorithm-class-name=com.demo.sharding.order.sharding.algorithm.OrderDbShardingByUserAlgorithm
#C端分表策略
spring.shardingsphere.sharding.tables.order_item_detail_sharded_by_user_id_.actual-data-nodes=master$->{0..7}.order_item_detail_sharded_by_user_id_$->{0..7}
#分片字段
spring.shardingsphere.sharding.tables.order_item_detail_sharded_by_user_id_.table-strategy.complex.sharding-columns=order_no,user_id
#order_item_detail表分片策略类
spring.shardingsphere.sharding.tables.order_item_detail_sharded_by_user_id_.table-strategy.complex.algorithm-class-name=com.demo.sharding.order.sharding.algorithm.OrderTableShardingByUserAlgorithm
#主键名称,缺省表示不使用自增主键生成器
spring.shardingsphere.sharding.tables.order_item_detail_sharded_by_user_id_.key-generator.column=id
#主键生成器类型,内置类型:SNOWFLAKE/UUID
spring.shardingsphere.sharding.tables.order_item_detail_sharded_by_user_id_.key-generator.type=SNOWFLAKE
#工作机器 id
spring.shardingsphere.sharding.tables.order_item_detail_sharded_by_user_id_.key-generator.props.worker.id=${workerId}
#时钟回拨可以容忍的毫秒数,默认10ms
spring.shardingsphere.sharding.tables.order_item_detail_sharded_by_user_id_.key-generator.props.max.tolerate.time.difference.milliseconds=5
其中官网中关于数据分片的数据项配置说明如下:
//参考:https://shardingsphere.apache.org/document/4.1.1/cn/manual/sharding-jdbc/configuration/config-spring-boot/
spring.shardingsphere.datasource.names= #数据源名称,多数据源以逗号分隔
spring.shardingsphere.datasource.<data-source-name>.type= #数据库连接池类名称
spring.shardingsphere.datasource.<data-source-name>.driver-class-name= #数据库驱动类名
spring.shardingsphere.datasource.<data-source-name>.url= #数据库url连接
spring.shardingsphere.datasource.<data-source-name>.username= #数据库用户名
spring.shardingsphere.datasource.<data-source-name>.password= #数据库密码
spring.shardingsphere.datasource.<data-source-name>.xxx= #数据库连接池的其它属性
spring.shardingsphere.sharding.tables.<logic-table-name>.actual-data-nodes= #由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持inline表达式。缺省表示使用已知数据源与逻辑表名称生成数据节点,用于广播表(即每个库中都需要一个同样的表用于关联查询,多为字典表)或只分库不分表且所有库的表结构完全一致的情况
#分库策略,缺省表示使用默认分库策略,以下的分片策略只能选其一
#用于单分片键的标准分片场景
spring.shardingsphere.sharding.tables.<logic-table-name>.database-strategy.standard.sharding-column= #分片列名称
spring.shardingsphere.sharding.tables.<logic-table-name>.database-strategy.standard.precise-algorithm-class-name= #精确分片算法类名称,用于=和IN。该类需实现PreciseShardingAlgorithm接口并提供无参数的构造器
spring.shardingsphere.sharding.tables.<logic-table-name>.database-strategy.standard.range-algorithm-class-name= #范围分片算法类名称,用于BETWEEN,可选。该类需实现RangeShardingAlgorithm接口并提供无参数的构造器
#用于多分片键的复合分片场景
spring.shardingsphere.sharding.tables.<logic-table-name>.database-strategy.complex.sharding-columns= #分片列名称,多个列以逗号分隔
spring.shardingsphere.sharding.tables.<logic-table-name>.database-strategy.complex.algorithm-class-name= #复合分片算法类名称。该类需实现ComplexKeysShardingAlgorithm接口并提供无参数的构造器
#行表达式分片策略
spring.shardingsphere.sharding.tables.<logic-table-name>.database-strategy.inline.sharding-column= #分片列名称
spring.shardingsphere.sharding.tables.<logic-table-name>.database-strategy.inline.algorithm-expression= #分片算法行表达式,需符合groovy语法
#Hint分片策略
spring.shardingsphere.sharding.tables.<logic-table-name>.database-strategy.hint.algorithm-class-name= #Hint分片算法类名称。该类需实现HintShardingAlgorithm接口并提供无参数的构造器
#分表策略,同分库策略
spring.shardingsphere.sharding.tables.<logic-table-name>.table-strategy.xxx= #省略
spring.shardingsphere.sharding.tables.<logic-table-name>.key-generator.column= #自增列名称,缺省表示不使用自增主键生成器
spring.shardingsphere.sharding.tables.<logic-table-name>.key-generator.type= #自增列值生成器类型,缺省表示使用默认自增列值生成器。可使用用户自定义的列值生成器或选择内置类型:SNOWFLAKE/UUID
spring.shardingsphere.sharding.tables.<logic-table-name>.key-generator.props.<property-name>= #属性配置, 注意:使用SNOWFLAKE算法,需要配置worker.id与max.tolerate.time.difference.milliseconds属性。若使用此算法生成值作分片值,建议配置max.vibration.offset属性
spring.shardingsphere.sharding.binding-tables[0]= #绑定表规则列表
spring.shardingsphere.sharding.binding-tables[1]= #绑定表规则列表
spring.shardingsphere.sharding.binding-tables[x]= #绑定表规则列表
spring.shardingsphere.sharding.broadcast-tables[0]= #广播表规则列表
spring.shardingsphere.sharding.broadcast-tables[1]= #广播表规则列表
spring.shardingsphere.sharding.broadcast-tables[x]= #广播表规则列表
spring.shardingsphere.sharding.default-data-source-name= #未配置分片规则的表将通过默认数据源定位
spring.shardingsphere.sharding.default-database-strategy.xxx= #默认数据库分片策略,同分库策略
spring.shardingsphere.sharding.default-table-strategy.xxx= #默认表分片策略,同分表策略
spring.shardingsphere.sharding.default-key-generator.type= #默认自增列值生成器类型,缺省将使用org.apache.shardingsphere.core.keygen.generator.impl.SnowflakeKeyGenerator。可使用用户自定义的列值生成器或选择内置类型:SNOWFLAKE/UUID
spring.shardingsphere.sharding.default-key-generator.props.<property-name>= #自增列值生成器属性配置, 比如SNOWFLAKE算法的worker.id与max.tolerate.time.difference.milliseconds
spring.shardingsphere.sharding.master-slave-rules.<master-slave-data-source-name>.master-data-source-name= #详见读写分离部分
spring.shardingsphere.sharding.master-slave-rules.<master-slave-data-source-name>.slave-data-source-names[0]= #详见读写分离部分
spring.shardingsphere.sharding.master-slave-rules.<master-slave-data-source-name>.slave-data-source-names[1]= #详见读写分离部分
spring.shardingsphere.sharding.master-slave-rules.<master-slave-data-source-name>.slave-data-source-names[x]= #详见读写分离部分
spring.shardingsphere.sharding.master-slave-rules.<master-slave-data-source-name>.load-balance-algorithm-class-name= #详见读写分离部分
spring.shardingsphere.sharding.master-slave-rules.<master-slave-data-source-name>.load-balance-algorithm-type= #详见读写分离部分
spring.shardingsphere.props.sql.show= #是否开启SQL显示,默认值: false
spring.shardingsphere.props.executor.size= #工作线程数量,默认值: CPU核数
(2)编写C端维度分库路由算法类
该算法类已配置在application.properties中。
//C端维度分库路由算法类
public class OrderDbShardingByUserAlgorithm implements ComplexKeysShardingAlgorithm<Comparable<?>> {
@Override
public Collection<String> doSharding(Collection<String> dbs, ComplexKeysShardingValue<Comparable<?>> shardingValue) {
Collection<Comparable<?>> orderNos = shardingValue.getColumnNameAndShardingValuesMap().get("order_no");
Collection<Comparable<?>> userIds = shardingValue.getColumnNameAndShardingValuesMap().get("user_id");
Set<String> actualDbNames = null;
if (CollectionUtils.isNotEmpty(orderNos)) {
actualDbNames = orderNos.stream()
.map(orderNo -> getActualDbName(String.valueOf(orderNo), dbs))
.collect(Collectors.toSet());
} else if (CollectionUtils.isNotEmpty(userIds)) {
actualDbNames = userIds.stream()
.map(userId -> getActualDbName(String.valueOf(userId), dbs))
.collect(Collectors.toSet());
}
return actualDbNames;
}
public String getActualDbName(String shardingValue, Collection<String> dbs) {
//获取userId后三位
String userIdSuffix = StringUtils.substring(shardingValue, shardingValue.length() - 3);
//使用userId后三位进行路由
int dbSuffix = userIdSuffix.hashCode() % dbs.size();
for (String db : dbs) {
if (db.endsWith(String.valueOf(dbSuffix))) {
return db;
}
}
return null;
}
}
(3)编写C端维度分表路由算法类
该算法类已配置在application.properties中。
//C端维度分表路由算法类
public class OrderTableShardingByUserAlgorithm implements ComplexKeysShardingAlgorithm<Comparable<?>> {
@Override
public Collection<String> doSharding(Collection<String> tables, ComplexKeysShardingValue<Comparable<?>> shardingValue) {
Collection<Comparable<?>> orderNos = shardingValue.getColumnNameAndShardingValuesMap().get("order_no");
Collection<Comparable<?>> userIds = shardingValue.getColumnNameAndShardingValuesMap().get("user_id");
Set<String> actualTableNames = null;
if (CollectionUtils.isNotEmpty(orderNos)) {
actualTableNames = orderNos.stream()
.map(orderNo -> getActualTableName(String.valueOf(orderNo), tables))
.collect(Collectors.toSet());
} else if (CollectionUtils.isNotEmpty(userIds)) {
actualTableNames = userIds.stream()
.map(userId -> getActualTableName(String.valueOf(userId), tables))
.collect(Collectors.toSet());
}
return actualTableNames;
}
public String getActualTableName(String shardingValue, Collection<String> tables) {
//获取userId后三位
String userIdSuffix = StringUtils.substring(shardingValue, shardingValue.length() - 3);
//使用userId后三位进行路由
int tableSuffix = userIdSuffix.hashCode() / tables.size() % tables.size();
for (String table : tables) {
if (table.endsWith(String.valueOf(tableSuffix))) {
return table;
}
}
return null;
}
}
(4)创建订单的具体实现
@RestController
@RequestMapping(value = "/user/order")
public class UserOrderController {
@Autowired
private UserOrderInfoService userOrderInfoService;
...
//创建订单
@PostMapping("/generateOrder")
public OperationResponse generateOrder(@RequestBody UserOrderInfoRequest userOrderInfoRequest) {
try {
long startTime = System.currentTimeMillis();
userOrderInfoService.generateOrder(userOrderInfoRequest);
long endTime = System.currentTimeMillis();
log.info("创建用户订单耗时:[{}]", (endTime - startTime));
return OperationResponse.success(OrderCode.ADD_ORDER_SUCCESS.getDesc());
} catch (BaseException e) {
return OperationResponse.error(e.getMessage());
} catch (Exception e) {
log.error("generateOrder error: [{}]", e.getMessage(), e);
return OperationResponse.error(OrderCode.ADD_ORDER_ERROR.getDesc());
}
}
...
}
//用户订单服务实现
@Service
public class UserOrderInfoServiceImpl implements UserOrderInfoService {
@Autowired
private UserOrderRepository userOrderRepository;
...
@Override
public void generateOrder(UserOrderInfoRequest userOrderInfoRequest) {
//1.入参校验
OrderValidation.checkVerifyOrderRequest(userOrderInfoRequest);
//2.添加订单信息
OrderInfo orderInfo = orderConvertor.dtoConvertOrderInfo(userOrderInfoRequest);
//3.添加订单详情
List<OrderItemDetail> orderItemDetailList = userOrderInfoRequest.getOrderItemDetailList();
//4.生成订单号
String orderNo = GenerateOrderNoUtils.getOrderNo(orderInfo.getUserId(), orderInfo.getMerchantId());
//5.为订单和订单明细设置订单号
orderInfo.setOrderNo(orderNo);
orderItemDetailList.forEach(orderItemDetail -> orderItemDetail.setOrderNo(orderInfo.getOrderNo()));
//6.保存订单信息
userOrderRepository.generateOrderInfo(orderInfo, orderItemDetailList);
...
}
...
}
public class GenerateOrderNoUtils {
...
//生成订单单号编码(调用方法),网站中该用户唯一ID 防止重复
public static String getOrderNo(Long userId, Long merchantId) {
return getOrderIdKey(String.valueOf(userId), String.valueOf(merchantId));
}
private static String getOrderIdKey(String userId, String merchantId) {
return "10" + getDateTimeKey() + getAutoNoKey() + toCode(userId, merchantId);
}
//生成时间戳
private static String getDateTimeKey() {
return DateFormatUtils.format(new Date(), "yyMMdd");
}
private static String getAutoNoKey() {
long random = getRandom(6);
return String.valueOf(genNo(random, 5));
}
//根据ID进行加密 + 加随机数组成固定长度编码:3位随机数 + 商家ID后3位 + 用户ID后3位
private static String toCode(String userId, String merchantId) {
String userIdSubstring = userId.substring(userId.length() - 3);
String merchantIdSubstring = merchantId.substring(merchantId.length() - 3);
long random = getRandom(3);
return random + merchantIdSubstring + userIdSubstring;
}
...
}
@Repository
public class UserOrderRepository {
...
@Transactional(rollbackFor = Exception.class)
public void generateOrderInfo(OrderInfo orderInfo, List<OrderItemDetail> orderItemDetailList) {
//1.保存订单信息
userOrderInfoMapper.insertSelective(orderInfo);
//2.批量保存订单明细
userOrderItemDetailMapper.batchInsert(orderItemDetailList);
}
...
}
而在userOrderInfoMapper.xml和userOrderItemDetailMapper.xml中已变为逻辑SQL。这些逻辑SQL会交给ShardingSphere-JDBC进行处理:解析SQL、进行拦截路由拼装、生成具体的SQL。ShardingSphere-JDBC会最终定位到具体的哪个库和哪个表中执行具体的SQL。
####################################userOrderInfoMapper.xml####################################
<insert id="insertSelective" parameterType="com.demo.sharding.order.domain.entity.OrderInfo" keyProperty="orderInfo.id"
useGeneratedKeys="true">
insert into order_info_sharded_by_user_id_
<trim prefix="(" suffix=")" suffixOverrides=",">
...
</trim>
</insert>
####################################userOrderItemDetailMapper.xml####################################
<insert id="batchInsert">
insert into order_item_detail_sharded_by_user_id_
(order_no,
product_id,category_id,goods_num,goods_price,goods_amount,discount_amount,discount_id,create_user,update_user)
values
<foreach collection="records" item="record" separator=",">
(#{record.orderNo,jdbcType=VARCHAR}, #{record.productId,jdbcType=BIGINT},
#{record.categoryId,jdbcType=BIGINT}, #{record.goodsNum,jdbcType=BIGINT},
#{record.goodsPrice,jdbcType=DECIMAL},#{record.goodsAmount,jdbcType=DECIMAL},
#{record.discountAmount,jdbcType=DECIMAL}, #{record.discountId,jdbcType=BIGINT},
#{record.createUser,jdbcType=BIGINT},#{record.updateUser,jdbcType=BIGINT})
</foreach>
</insert>
18.C端ShardingSphere读写分离实现
只需在application.properties进行读写分离配置:
#主从配置-读写分离
spring.shardingsphere.sharding.master-slave-rules.master0.master-data-source-name=master0
spring.shardingsphere.sharding.master-slave-rules.master0.slave-data-source-names=slave0
spring.shardingsphere.sharding.master-slave-rules.master1.master-data-source-name=master1
spring.shardingsphere.sharding.master-slave-rules.master1.slave-data-source-names=slave1
spring.shardingsphere.sharding.master-slave-rules.master2.master-data-source-name=master2
spring.shardingsphere.sharding.master-slave-rules.master2.slave-data-source-names=slave2
spring.shardingsphere.sharding.master-slave-rules.master3.master-data-source-name=master3
spring.shardingsphere.sharding.master-slave-rules.master3.slave-data-source-names=slave3
spring.shardingsphere.sharding.master-slave-rules.master4.master-data-source-name=master4
spring.shardingsphere.sharding.master-slave-rules.master4.slave-data-source-names=slave4
spring.shardingsphere.sharding.master-slave-rules.master5.master-data-source-name=master5
spring.shardingsphere.sharding.master-slave-rules.master5.slave-data-source-names=slave5
spring.shardingsphere.sharding.master-slave-rules.master6.master-data-source-name=master6
spring.shardingsphere.sharding.master-slave-rules.master6.slave-data-source-names=slave6
spring.shardingsphere.sharding.master-slave-rules.master7.master-data-source-name=master7
spring.shardingsphere.sharding.master-slave-rules.master7.slave-data-source-names=slave7
其中官网中关于读写分离的数据项配置说明如下:
//参考:https://shardingsphere.apache.org/document/4.1.1/cn/manual/sharding-jdbc/configuration/config-spring-boot/
spring.shardingsphere.sharding.master-slave-rules.<master-slave-data-source-name>.master-data-source-name= #主库数据源名称
spring.shardingsphere.sharding.master-slave-rules.<master-slave-data-source-name>.slave-data-source-names[0]= #从库数据源名称列表
spring.shardingsphere.sharding.master-slave-rules.<master-slave-data-source-name>.slave-data-source-names[1]= #从库数据源名称列表
spring.shardingsphere.sharding.master-slave-rules.<master-slave-data-source-name>.slave-data-source-names[x]= #从库数据源名称列表
spring.shardingsphere.sharding.master-slave-rules.<master-slave-data-source-name>.load-balance-algorithm-class-name= #从库负载均衡算法类名称。该类需实现MasterSlaveLoadBalanceAlgorithm接口且提供无参数构造器
spring.shardingsphere.sharding.master-slave-rules.<master-slave-data-source-name>.load-balance-algorithm-type= #从库负载均衡算法类型,可选值:ROUND_ROBIN,RANDOM。若`load-balance-algorithm-class-name`存在则忽略该配置
spring.shardingsphere.props.sql.show= #是否开启SQL显示,默认值: false
spring.shardingsphere.props.executor.size= #工作线程数量,默认值: CPU核数
spring.shardingsphere.props.check.table.metadata.enabled= #是否在启动时检查分表元数据一致性,默认值: false
19.异构订单实现
要实现异构订单,就需要在C端创建完订单后,发送消息到MQ,然后由B端进行消费。
@Service
public class UserOrderInfoServiceImpl implements UserOrderInfoService {
...
@Override
public void generateOrder(UserOrderInfoRequest userOrderInfoRequest) {
//1.入参校验
OrderValidation.checkVerifyOrderRequest(userOrderInfoRequest);
//2.添加订单信息
OrderInfo orderInfo = orderConvertor.dtoConvertOrderInfo(userOrderInfoRequest);
//3.添加订单详情
List<OrderItemDetail> orderItemDetailList = userOrderInfoRequest.getOrderItemDetailList();
//4.生成订单号
String orderNo = GenerateOrderNoUtils.getOrderNo(orderInfo.getUserId(), orderInfo.getMerchantId());
//5.为订单和订单明细设置订单号
orderInfo.setOrderNo(orderNo);
orderItemDetailList.forEach(orderItemDetail -> orderItemDetail.setOrderNo(orderInfo.getOrderNo()));
//6.保存订单信息
userOrderRepository.generateOrderInfo(orderInfo, orderItemDetailList);
//7.构建消息体
OrderSyncMessage orderSyncMessage = OrderSyncMessage.builder()
.requestSource(RequestSource.C)
.orderOperateType(OrderOperateType.add)
.orderNo(orderInfo.getOrderNo())
.build();
//8.发送mq消息
rocketMQProducer.reliablySend(topic, orderSyncMessage, orderSyncMessage.getOrderNo(), obj -> {
OrderSyncMessage message = (OrderSyncMessage) obj;
// 这里可以将消息存储到db,然后由后台线程定时重试,确保消息一定到达Broker
log.info("generateOrder send mq message failed, store the mq message to db,orderNo:[{}],message:[{}]", message.getOrderNo(), message);
});
}
...
}
消费消息创建异构订单:
@Component
@RocketMQMessageListener(consumerGroup = "${order.sync.consumer.group}", topic = "${order.topic}")
public class OrderSyncConsumer implements RocketMQListener<OrderSyncMessage> {
@Autowired
private MerchantOrderRepository merchantOrderRepository;
@Autowired
private UserOrderRepository userOrderRepository;
@Override
public void onMessage(OrderSyncMessage orderSyncMessage) {
log.info("received message,start processing,orderNo:[{}],message:[{}]", orderSyncMessage.getOrderNo(), orderSyncMessage);
//1.check请求来源
if (Objects.isNull(orderSyncMessage.getRequestSource())) {
log.error("requestSource can not be empty,orderNo:[{}]", orderSyncMessage.getOrderNo());
return;
}
//2.获取订单源数据
OrderDetailVO orderDetail = getOrderDetail(orderSyncMessage.getOrderNo(), orderSyncMessage.getRequestSource());
//3.如果订单为空,打印日志并抛出异常,从而等待rocketMQ下次重试
if (Objects.isNull(orderDetail)) {
//打印错误日志
log.error("orderDetail not exist,orderNo:[{}]", orderSyncMessage.getOrderNo());
//抛出异常,等待rocketMQ下次重试
throw new NullPointerException("orderDetail is null,orderNo:[{}]" + orderSyncMessage.getOrderNo());
}
//4.获取要处理的目标源
RequestSource target = RequestSource.C.equals(orderSyncMessage.getRequestSource()) ? RequestSource.B : RequestSource.C;
//5.同步订单数据的处理,即异构一份订单数据到目标源 或 同步订单状态到目标源
if (OrderOperateType.add.equals(orderSyncMessage.getOrderOperateType())) {
//异构一份订单数据到目标源
syncOrderInfo(orderSyncMessage, orderDetail, target);
} else if (OrderOperateType.updateOrderStatus.equals(orderSyncMessage.getOrderOperateType())) {
//同步订单状态到目标源
updateOrderStatus(orderDetail.getOrderNo(), orderDetail.getOrderStatus(), target);
}
}
//异构一份订单数据到目标源
private void syncOrderInfo(OrderSyncMessage orderSyncMessage, OrderDetailVO orderDetail, RequestSource target) {
//1.幂等处理,目标订单存在则跳过
OrderDetailVO targetOrder = getOrderDetail(orderSyncMessage.getOrderNo(), target);
if (Objects.nonNull(targetOrder)) {
log.info("targetOrder existed,orderNo:[{}]", orderSyncMessage.getOrderNo());
return;
}
//2.同步订单信息到目标源
syncOrderInfo(orderDetail, target);
}
//同步订单信息到目标源
private void syncOrderInfo(OrderDetailVO orderDetail, RequestSource target) {
//1.拷贝数据 orderDetail -> orderInfo
OrderInfo orderInfo = new OrderInfo();
BeanUtils.copyProperties(orderDetail, orderInfo);
//2.转换类型 OrderItemDetailDto -> OrderItemDetail
List<OrderItemDetail> orderItemDetailList = convertOrderItemDetails(orderDetail.getOrderItemDetails());
//3.异构订单到商家,目前只有从C端下单,异构到B端的业务
if (RequestSource.B.equals(target)) {
//异构订单到商家
merchantOrderRepository.syncOrderInfo(orderInfo, orderItemDetailList);
}
}
//获取订单详情
private OrderDetailVO getOrderDetail(String orderNo, RequestSource requestSource) {
if (RequestSource.C.equals(requestSource)) {
return userOrderRepository.getOrderDetail(orderNo);
} else if (RequestSource.B.equals(requestSource)) {
return merchantOrderRepository.getOrderDetail(orderNo);
}
return null;
}
//同步订单状态到目标源
private void updateOrderStatus(String orderNo, Integer orderStatus, RequestSource requestSource) {
if (RequestSource.C.equals(requestSource)) {
userOrderRepository.updateStatus(orderNo, orderStatus);
} else if (RequestSource.B.equals(requestSource)) {
merchantOrderRepository.updateStatus(orderNo, orderStatus);
}
}
//将OrderItemDetailDto类型 转换为 OrderItemDetail类型
private List<OrderItemDetail> convertOrderItemDetails(List<OrderItemDetailDto> orderItemDetailDtos) {
List<OrderItemDetail> orderItemDetailList = new ArrayList<>();
for (OrderItemDetailDto itemDetail : orderItemDetailDtos) {
OrderItemDetail orderItemDetail = new OrderItemDetail();
BeanUtils.copyProperties(itemDetail, orderItemDetail);
orderItemDetailList.add(orderItemDetail);
}
return orderItemDetailList;
}
}
20.单库亿级数据怎么迁移到多库多表上
(1)单库单表的亿级数据如何迁移到8库8表上
(2)全量同步的处理
(3)数据校验的处理
(4)增量同步的处理
(5)增量同步的问题
(1)单库单表的亿级数据如何迁移到8库8表上
数据迁移 -> 全量同步
数据校验的问题
断点续传的问题
数据迁移 -> 增量同步
(2)全量同步的处理
全量同步,指的就是将旧库的数据,通过ShardingShpere的路由,同步迁移到新库当中。单库的数据特别大,有上亿条数据,我们并不是一下子全部查出来,而是分批次去查询。
全量同步的处理流程如下:
每当需要进行一次全量同步时,就会创建一条迁移记录。这条迁移记录其实就代表着一次全量同步的任务,对线上的数据进行迁移可能会执行很多次这种全量同步的任务。在一次全量同步的任务中,会对全量数据进行循环式、分批次、每次尝试查询500条数据。如果能从单库单表查询到数据,就会添加一条该批次的迁移明细记录。接着可以对每次查询出来的500条数据,按照时间等条件进行过滤。然后将过滤出来的数据按表进行分组,批量插入到多库多表中。最后更新该批次的迁移明细记录的状态为同步状态。如果在对全量数据进行循环式、每次尝试查询500条数据时,终于查不到数据了,则更新本次全量同步任务对应的迁移记录的状态为同步状态。
(3)数据校验的处理
情况一: 旧库有某数据而新库没有,直接在新库中插入数据,保证一致性。
情况二: 旧库和新库中都有某数据,但旧库的更新时间更大,新库需要更新。
情况三: 旧库和新库中都有某数据,但新库的更新时间更大,新库无需操作。
(4)增量同步的处理
当已经进行完全量同步将旧库的数据都同步到新库时,由于数据迁移和开发的过程是一个平滑的过程,旧库在全量同步时还在承担业务的访问处理,新的数据还会打到旧库中。一旦进行增删改操作,还是需要进行同步的,这就是增量同步。
增量同步的处理流程如下:
引入Canal监听旧库,一旦旧库有增删改操作,Canal就可以监听到并把binlog日志发送到MQ中。这时便可以通过定时任务去获取MQ的binlog日志消息,然后在新库中进行重做binlog日志,从而保证新库和旧库数据的一致性。
(5)增量同步的问题
问题一: 如何保证MQ消息不丢失
问题二: 数据的合并问题,上一秒新增了数据,下一秒修改了数据,此时需要数据合并