雪花算法
1.雪花算法分布式id的特点
- 全局唯一性:最基本的东西
- 递增:方便索引,提升索引性能
- 高可用:任何时候都能生成正确到的ID
- 高性能:并发环境依旧健壮(短时间能生成大量ID,一台机器一毫秒产4906个不同ID)
- 分布式:分布式不会产生重复的ID
2.雪花算法概要
SnowFlake生成的ID是8Byte64位的(在java中是long)结构中
1.首位1位不用:生成的ID总是整数,没有实际作用
2.时间戳41位:是递增的关键,表示正整数是2^41-1毫秒,69年。
3.工作id10位:可以部署2^10=1024个工作节点,5位datacenterID和5位workerID
4.序列号12位:用来存储统一毫秒产生的ID,2^12-1=4095,同一毫秒产生4095个序号
3.代码实现类
public class IdWorker{
private long workerId;
private long datacenterId;
private long sequence;
/**
*
* @param workerId 工作id(0-31)
* @param datacenterId 数据中心/机房id(0-31)
* @param sequence 未设置最大值,取12bit
*/
public IdWorker(long workerId, long datacenterId, long sequence){
// sanity check for workerId
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0",maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0",maxDatacenterId));
}
System.out.printf("worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d",
timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId);
this.workerId = workerId;
this.datacenterId = datacenterId;
this.sequence = sequence;
}
private long twepoch = 1288834974657L;
private long workerIdBits = 5L;
private long datacenterIdBits = 5L;
private long maxWorkerId = -1L ^ (-1L << workerIdBits);
private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
private long sequenceBits = 12L;
private long workerIdShift = sequenceBits;
private long datacenterIdShift = sequenceBits + workerIdBits;
private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
private long sequenceMask = -1L ^ (-1L << sequenceBits);
private long lastTimestamp = -1L;
public long getWorkerId(){
return workerId;
}
public long getDatacenterId(){
return datacenterId;
}
public long getTimestamp(){
return System.currentTimeMillis();
}
public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
System.err.printf("clock is moving backwards. Rejecting requests until %d.", lastTimestamp);
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds",
lastTimestamp - timestamp));
}
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0;
}
lastTimestamp = timestamp;
return ((timestamp - twepoch) << timestampLeftShift) |
(datacenterId << datacenterIdShift) |
(workerId << workerIdShift) |
sequence;
}
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
private long timeGen(){
return System.currentTimeMillis();
}
//---------------测试---------------
public static void main(String[] args) {
IdWorker worker = new IdWorker(1,1,1);
for (int i = 0; i < 30; i++) {
System.out.println(worker.nextId());
}
}
}
4.编写mybatis插件
编写类实现org.apache.ibatis.plugin.Interceptor接口,再在mybatis SqlSessionFactory
中增加插件(拦截器)。
package com.ccity;
import cn.webrx.Book;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Executors;
/**
* @author 86155
* @version 17
* @title:ccity
* @projectName mybatis2022
* @description: TODO
* @date 2022/4/1320:02
* @since 1.0
*/
@Component
//args:你需要mybatis传入什么参数给你 type:你需要拦截的对象, method:要拦截的方法
@Intercepts({@Signature(type = Executor.class,method = "update",
args ={MappedStatement.class,Object.class})} )
public class SnoFlakeInterceptor implements Interceptor {
@Autowired
IdWorker idw;
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object target = invocation.getTarget();
MappedStatement ms= (MappedStatement) invocation.getArgs()[0];
if ("INSERT".equalsIgnoreCase(ms.getSqlCommandType().toString())) {
System.out.println("----------------------------------");
System.out.println(target.getClass().getName());//org.apache.ibatis.executor.CachingExecutor
System.out.println(invocation.getMethod().getName());//update
System.out.println(invocation.getMethod().getReturnType());//int
System.out.println(List.of(invocation.getArgs()));//[org.apache.ibatis.mapping.MappedStatement@2a49fe, Book(id=15, name=死神, man=久保带人)]
Object obj = invocation.getArgs()[1];
System.out.println(ms.getId());//cn.webrx.mapper.BookMapper.updateByPrimaryKey
System.out.println(ms.getSqlCommandType());//UPDATE
if (obj instanceof Book)
{
System.out.println("book---------------------");
System.out.println(Long.MAX_VALUE);
((Book) obj).setId(idw.nextId());
}
else if (obj instanceof HashMap)
{
System.out.println("map----------------------");
Map<String,Object> map= (Map<String, Object>) obj;
map.put("id",idw.nextId());
}
else {
System.out.println(obj.getClass());
Method m=obj.getClass().getMethod("setId",long.class);//反射获取数据库添加语句中参数类的setId方法
m.invoke(obj,idw.nextId());
System.out.println("reflect-------------------");
}
System.out.println("----------------------------------");
}
return invocation.proceed();
}
}
5.在mybatis配置中添加插件
package cn.webrx.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.ccity.IdWorker;
import com.ccity.SnoFlakeInterceptor;
import com.github.pagehelper.PageInterceptor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Signature;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.*;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import java.io.IOException;
import java.util.concurrent.Executors;
@Configuration
//加载resources/db.properties 配置文件
@PropertySource("classpath:db.properties")
//扫描有Mapper注解的接口 @Mapper 主要是解决,单元测试时报红
@ComponentScan("cn.webrx.mapper")
//也是扫描包,在接口上可以不加@Mapper
@tk.mybatis.spring.annotation.MapperScan("cn.webrx")
public class MybatisConfig {
@Value("${db.driver:com.mysql.cj.jdbc.Driver}")//默认值
private String driver;
@Value("${db.url:jdbc:mysql:/db}")
private String url;
@Value("${db.username:admin}")
private String username;
@Value("${db.password:123}")
private String password;
@Autowired
private SnoFlakeInterceptor sfi;
@Bean(name = "ds", initMethod = "init", destroyMethod = "close")
@Primary
public DruidDataSource ds() {
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName(driver);
ds.setUsername(username);
ds.setPassword(password);
ds.setUrl(url);
return ds;
}
@Bean(name = "sf")
@Primary
public SqlSessionFactoryBean sf(DruidDataSource ds) throws IOException {
SqlSessionFactoryBean sf = new SqlSessionFactoryBean();
sf.setDataSource(ds);
// resources/mapper/XxxMapper.xml
sf.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*Mapper*.xml"));
//添加雪花算法插件拦截器
sf.setPlugins(sfi);
//sf.setPlugins(new SnoFlakeInterceptor());
//扫描定义别名 cn.webrx.entity.Book 别名为 book
sf.setTypeAliasesPackage("cn.webrx");
return sf;
}
@Bean("idw") @Primary
public IdWorker idWorker(){
return new IdWorker(1,1,4099);
}
@Bean("sfi")
public SnoFlakeInterceptor sfi(){
return new SnoFlakeInterceptor();
}
}
6.进行测试
@SpringJUnitConfig(SpringConfig.class)
public class MyTest {
@Autowired(required = false)
DbMapper dm;
@Autowired
BookMapper bm;
@Autowired
IdWorker idWorker;
@Autowired
StudentMapper sm;
@Test @DisplayName("添加学生,测试雪花插件健壮性")
public void m5(){
Student student = new Student();
student.setAge(22);
student.setName("张良");
student.setGrade("法学院");
sm.insert(student);
System.out.println(student.getId());
}
}
测试结果: