中间件
常用中间件
MyCat
- 是基于 Proxy,它复写了 MySQL 协议,将 Mycat Server 伪装成⼀个 MySQL 数据库
- 客户端所有的jdbc请求都必须要先交给MyCat,再有 MyCat转发到具体的真实服务器
- 缺点是效率偏低,中间包装了⼀层
- 代码⽆侵⼊性
ShardingSphere下的Sharding-JDBC
- 基于jdbc驱动,不⽤额外的proxy,在本地应⽤层重写Jdbc 原⽣的⽅法,实现数据库分⽚形式
- 是基于 JDBC 接⼝的扩展,是以 jar 包的形式提供轻量级服务的,性能⾼
- 代码有侵⼊性
两者的区别和相同点
相同点:
流程都是SQL解析–>SQL路由–>SQL改
写–>结果归并
区别:
ShardingSphere
简介
- 是⼀套开源的分布式数据库解决⽅案组成的⽣态圈,定位为 Database Plus
- 它由 JDBC、Proxy 和 Sidecar这 3款既能够独⽴部署,⼜⽀持混合部署配合使⽤的产品组成
三大构成
ShardingSphere-Sidecar
ShardingSphere-JDBC
ShardingSphere-Proxy
常见分片策略
分片策略是分片键和分片算法的组合策略,真正用于实现数据分片操作的是分片键与相应的分片算法。在分片策略中,分片键确定了数据的拆分依据,分片算法则决定了如何对分片键值运算,将数据路由到哪个物理分片中。
在分表和分库时使用分片策略和分片算法的方式是一致的。
1、标准分片策略 standard
只⽀持【单分⽚键】
- PreciseShardingAlgorithm 精准分⽚ 是必选的,⽤于处理=和IN的分⽚
- RangeShardingAlgorithm 范围分配 是可选的,⽤于处理BETWEEN AND、>、<、>=、<=等范围操作符
spring:
shardingsphere:
rules:
sharding:
tables:
t_order: # 逻辑表名称
# 数据节点:数据库.分片表
actual-data-nodes: db$->{0..1}.t_order_${1..10}
# 分库策略
databaseStrategy: # 分库策略
standard: # 用于单分片键的标准分片场景
shardingColumn: order_id # 分片列名称
shardingAlgorithmName: # 分片算法名称
tableStrategy: # 分表策略,同分库策略
2、⾏表达式分⽚策略 inline
只⽀持【单分⽚键】使⽤Groovy的表达式,提供对SQL语句中的 =和IN 的分⽚操作⽀持
spring:
shardingsphere:
rules:
sharding:
tables:
t_order: # 逻辑表名称
# 数据节点:数据库.分片表
actual-data-nodes: db$->{0..1}.t_order_${1..10}
# 分库策略
databaseStrategy: # 分库策略
inline: # 行表达式类型分片策略
algorithm-expression: db$->{order_id % 2} Groovy表达式
tableStrategy: # 分表策略,同分库策略
3、复合分⽚策略 complex
⽀持【多分⽚键】,提供对SQL语句中的=, IN和BETWEEN AND的分⽚操作⽀持
比如:我们希望通过user_id和order_id等多个字段共同运算得出数据路由到具体哪个分片中,就可以应用该策略。
spring:
shardingsphere:
rules:
sharding:
tables:
t_order: # 逻辑表名称
# 数据节点:数据库.分片表
actual-data-nodes: db$->{0..1}.t_order_${1..10}
# 分库策略
databaseStrategy: # 分库策略
complex: # 用于多分片键的复合分片场景
shardingColumns: order_id,user_id # 分片列名称,多个列以逗号分隔
shardingAlgorithmName: # 分片算法名称
tableStrategy: # 分表策略,同分库策略
4、Hint分⽚策略 hint
Hint强制分片策略相比于其他几种分片策略稍有不同,该策略无需配置分片健,由外部指定分库和分表的信息,可以让SQL在指定的分库、分表中执行。
使用场景:
分片字段不存在SQL和数据库表结构中,而存在于外部业务逻辑。
强制在指定数据库进行某些数据操作。
比如,我们希望用user_id做分片健进行路由订单数据,但是t_order表中也没user_id这个字段啊,这时可以通过Hint API手动指定分片库、表等信息,强制让数据插入指定的位置。
spring:
shardingsphere:
rules:
sharding:
tables:
t_order: # 逻辑表名称
# 数据节点:数据库.分片表
actual-data-nodes: db$->{0..1}.t_order_${1..10}
# 分库策略
databaseStrategy: # 分库策略
hint: # Hint 分片策略
shardingAlgorithmName: # 分片算法名称
tableStrategy: # 分表策略,同分库策略
分片算法
springboot整合sharding-jdbc
添加依赖
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
</dependency>
配置文件
# 数据源 ds0 第一个数据库
shardingsphere:
datasource:
#数据源名称
names: ds0
ds0:
connectionTimeoutMilliseconds: 30000
driver-class-name: com.mysql.cj.jdbc.Driver
idleTimeoutMilliseconds: 60000
jdbc-url: jdbc:mysql://120.79.150.146:3306/dcloud_account?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
maintenanceIntervalMilliseconds: 30000
maxLifetimeMilliseconds: 1800000
maxPoolSize: 50
minPoolSize: 50
password: 123
type: com.zaxxer.hikari.HikariDataSource
username: root
props:
# 打印执行的数据库以及语句
sql:
show: true
sharding:
tables:
traffic:
# 指定traffic表的数据分布情况,配置数据节点,行表达式标识符使用 ${...} 或 $->{...},但前者与 Spring 本身的文件占位符冲突,所以在 Spring 环境中建议使用 $->{...}
actual-data-nodes: ds0.traffic_$->{0..1}
#水平分表策略+行表达式分片
table-strategy:
inline:
algorithm-expression: traffic_$->{ account_no % 2 }
sharding-column: account_no
#id生成策略
key-generator:
column: id
props:
worker:
id: ${workId}
#id生成策略
type: SNOWFLAKE
分库分表暴露的问题-ID冲突
常见解决方案
数据库自增ID
设置不同的自增步长
auto_increment_offset、auto-increment-increment
缺点
依靠数据库系统的功能实现,但是未来扩容麻烦
主从切换时的不⼀致可能会导致重复发号
性能瓶颈存在单台sql上
UUID
性能⾮常⾼,没有⽹络消耗
缺点
⽆序的字符串,不具备趋势⾃增特性
UUID太⻓,不易于存储,浪费存储空间,很多场景不适⽤
Redis 发号器
利⽤Redis的INCR和INCRBY来实现,原⼦操作,线程安全,性能⽐Mysql强劲
缺点
需要占⽤⽹络资源,增加系统复杂度
SnowFlake雪花算法
twitter 开源的分布式 ID ⽣成算法,代码实现简单、不占⽤宽带、数据迁移不受影响
⽣成的 id 中包含有时间戳,所以⽣成的 id 按照时间递增
部署了多台服务器,需要保证系统时间⼀样,机器编号不⼀样
缺点
依赖系统时钟(多台服务器时间⼀定要⼀样)
SnowFlake雪花算法
简介
twitter⽤scala语⾔编写的⾼效⽣成唯⼀ID的算法
优点
⽣成的ID不重复
算法性能⾼
基于时间戳,基本保证有序递增
雪花算法⽣成的数字
- long类,所以就是8个byte,64bit
- ⽣成的唯⼀值⽤于数据库主键,不能是负数,所以值为
0~9223372036854775807(2的63次⽅-1)
两个重要的点
保证workId不能重复
解决方法:
自定义workId
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
@Configuration
@Slf4j
public class SnowFlakeWordIdConfig {
/**
* 动态指定sharding jdbc 的雪花算法中的属性work.id属性
* 通过调用System.setProperty()的方式实现,可用容器的 id 或者机器标识位
* workId最大值 1L << 100,就是1024,即 0<= workId < 1024
* {@link SnowflakeShardingKeyGenerator#getWorkerId()}
*
*/
static {
try {
InetAddress inetAddress = Inet4Address.getLocalHost();
String hostAddressIp = inetAddress.getHostAddress();
String workId = Math.abs(hostAddressIp.hashCode()) % 1024+"";
System.setProperty("workId",workId);
log.info("workId:{}",workId);
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
}
配置文件中
#id生成策略
key-generator:
column: id
props:
worker:
id: ${workId}
#id生成策略
type: SNOWFLAKE
时钟回拨
org.apache.shardingsphere.core.strategy.keygen.SnowflakeShardingKeyGenerator 已经解决了时钟回拨问题,看下源码
核心流程:
最后一次生成主键的时间 lastTime,和当前系统时间比较 currTime ,如果 lastTime < currTime ,则正常,如果lastTime > currTIme ,如果时间差在容忍范围内,则线程休眠时间差值,如果时间差大于容忍范围,则直接报异常。
public synchronized Comparable<?> generateKey() {
long currentMilliseconds = timeService.getCurrentMillis();
// 判断是否需要等待容忍时间差,如果需要,则等待时间差过去,再获取当前系统时间
if (this.waitTolerateTimeDifferenceIfNeed(currentMilliseconds)) {
currentMilliseconds = timeService.getCurrentMillis();
}
// 如果最后一次毫秒与当前系统时间毫秒相同,即还在同一毫秒内
if (this.lastMilliseconds == currentMilliseconds) {
/**
&位与运算符:两个数都转为二进制,如果相对应位都是1,则结果为1,否则为0
当序列为4095时,4095+1后的新序列与掩码进行位与运算结果是
当序列为其他值时,位与运算结果都不会是0
即本毫秒的序列已经用到最大值4096,此时要取下一个毫秒时间值*/
if (0L == (this.sequence = this.sequence + 1L & 4095L)) {
currentMilliseconds = this.waitUntilNextTime(currentMilliseconds);
}
} else {
// 上一毫秒已经过去,把序列值重置为1
this.vibrateSequenceOffset();
this.sequence = (long)this.sequenceOffset;
}
// 记录最新的时间戳
this.lastMilliseconds = currentMilliseconds;
/**
XX......xxx000000000000000000000000时间差XX
XXXXXXXXXX0000000000000 机器ID XX
XXXXXXXXXX序列号×序列号 xx
三部分进行|位或运算:如果相对应位都是0,则结果为0,否则为1
*/
return currentMilliseconds - EPOCH << 22 | this.getWorkerId() << 12 | this.sequence;
}
private boolean waitTolerateTimeDifferenceIfNeed(long currentMilliseconds) {
try {
// 如果获取ID时的最后一次时间毫秒数小于等于当前系统时间毫秒数,属于正常情况,则不需要等待
if (this.lastMilliseconds <= currentMilliseconds) {
return false;
} else {
// 时钟回拨的情况(生成序列的时间大于当前系统的时间),需要等等待时间差
long timeDifferenceMilliseconds = this.lastMilliseconds - currentMilliseconds;
// 获取ID时的最后一次毫秒数减去当前系统时间毫秒数的时间差
// 时间差小于最大容忍时间差,即当前还在时钟回拨的时间差之内
Preconditions.checkState(timeDifferenceMilliseconds < (long)this.getMaxTolerateTimeDifferenceMilliseconds(), "Clock is moving backwards, last time is %d milliseconds, current time is %d milliseconds", new Object[]{this.lastMilliseconds, currentMilliseconds});
// 线程休眠时间差
Thread.sleep(timeDifferenceMilliseconds);
return true;
}
} catch (Throwable var5) {
throw var5;
}
}
具体业务中,使用该方法生成唯一账号
import org.apache.shardingsphere.core.strategy.keygen.SnowflakeShardingKeyGenerator;
public class IDUtil {
private static SnowflakeShardingKeyGenerator shardingKeyGenerator = new SnowflakeShardingKeyGenerator();
/**
* 雪花算法生成器
* @return
*/
public static Comparable<?> geneSnowFlakeID(){
return shardingKeyGenerator.generateKey();
}
}
//生成唯一的账号
accountDO.setAccountNo(Long.valueOf(IDUtil.geneSnowFlakeID().toString()));