前言
雪花算法是64位的二进制组成,展示如下:
留给我们自定义的就只剩下机器id
和服务id号
了, 也就是说这中间的10位可以由我们自定义
虽然雪花算法重复概率已经非常低了,但是《墨菲定律》说:你不想发生的事,一定会发生
所以写代码不能抱有侥幸心理
思路
- 目前雪花算法主要生成数据库的ID,一般在分布式环境下,一个服务都是单独一个库,自定义中间10位的话,单服务最多可以部署1024个节点,一般服务应该达不到这么多节点(反正目前我没见过)
- 以服务名+序号存入redis,服务活跃时,参照
Redission
实现分布式锁一样,开一个线程做完看门狗来续期 - 用lua脚本获取一个唯一且有效的序列号,设置过期时间为30秒
- 获取序列化之后,开启一个守护线程,每10秒延长过期时间为30秒,服务健康就一直占用,服务关闭,这个Id会自动释放
实现
所有实现都参考reids实现分布式锁来做的
因为所有操作要保证原子性,所以获取服务Id
用lua
脚本实现
循环0-1023,获取一个空闲的序号,修改过期时间为30秒
local now = redis.call('TIME')[1]
local idWordsKey = KEYS[1]
local serviceName = KEYS[2]
local sp = ':'
for i = 0, 1023 do
local serviceKey = idWordsKey..sp..serviceName..sp..i
if redis.call('SETNX', serviceKey, now) == 1 then
redis.call('Expire', serviceKey, 30)
return i;
end
end
return -1
存入Redis格式是:
key=idWordsKey:serviceName:unm value=time
# 说明
idWordsKey :IdKey标识,可以随便命名
serviceName :服务名称
unm :服务序列号,取值范围是2^0-1 至 2^10-1,即1至1023
time :当前时间戳
例如:
java 代码
从Redis中获取空闲ID
/**
* key标识,可以随便定义
*/
private final String idWorker = "WorkerId";
/**
* 服务名称
*/
@Value("${app.idKey}")
private String serviceName;
/**
* lua 脚本
*/
private final String script =
"local now = redis.call('TIME')[1]\n" +
"local idWordsKey = KEYS[1]\n" +
"local serviceName = KEYS[2]\n" +
"local sp = ':'\n" +
"for i = 0, 1023 do\n" +
" local serviceKey = idWordsKey..sp..serviceName..sp..i\n" +
" if redis.call('SETNX', serviceKey, now) == 1 then\n" +
" redis.call('Expire', serviceKey, 30)\n" +
" return i;\n" +
" end\n" +
"end\n" +
"return -1";
@Resource
private RedisTemplate redisTemplate;
/**
* 获取机器标识号
* @param serviceName 服务名称
*/
private Long getWorkerIdNum(String serviceName) {
// 实例化脚本对象
DefaultRedisScript<Long> lua = new DefaultRedisScript<>();
lua.setResultType(Long.class);
lua.setScriptText(script);
List<String> keys = List.of(idWorker, serviceName);
// 获取序列号
Long num = (Long)redisTemplate.execute(lua, keys, keys.size());
String key = String.join(":", keys) + ":" + num;
// -1 代表机器用完了,重试
if (num < 0){
log.error("目前Id已用完,请重新启动试试");
System.exit(0);
}
// 自动续期
this.autoExpire(key);
return num;
}
/**
* 自动续期
*/
private void autoExpire(String key) {
ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1,
new BasicThreadFactory.Builder().namingPattern("id_auto_expire-%d").daemon(true).build());
executorService.scheduleAtFixedRate(() -> {
redisTemplate.expire(key, 30, TimeUnit.SECONDS);
log.info("自动续期id成功:{}", key);
}, 0, 10, TimeUnit.SECONDS);
}
单独引入
如果你想单独引入雪花算法,可以参考:https://gitee.com/yu120/sequence
Mybatis-plus
内部源码就是引入的这个工具类
Mybais-Plus引入
我是直接在Mybatia-plus
配置文件中直接初始化的,代码如下:
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @author chang
*/
@Slf4j
@Configuration
@EnableTransactionManagement
public class MybatisPlusConfig {
private final String idWorker = "WorkerId";
@Value("${app.idKey}")
private String serviceName;
private final String script =
"local now = redis.call('TIME')[1]\n" +
"local idWordsKey = KEYS[1]\n" +
"local serviceName = KEYS[2]\n" +
"local sp = ':'\n" +
"for i = 0, 1023 do\n" +
" local serviceKey = idWordsKey..sp..serviceName..sp..i\n" +
" if redis.call('SETNX', serviceKey, now) == 1 then\n" +
" redis.call('Expire', serviceKey, 30)\n" +
" return i;\n" +
" end\n" +
"end\n" +
"return -1";
@Resource
private RedisTemplate redisTemplate;
/**
* 分页插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
/**
* 初始化雪花ID
*/
@PostConstruct
public void initMybatisPlus(){
if (serviceName == null || serviceName.length() == 0){
log.error("雪花算法初始化失败,【 app.idKey 】配置为空。");
return;
}
long num = getWorkerIdNum(serviceName);
// 获取前5位
long workerId = num >> 5;
// 获取后5位
long dataCenterId = num & (~(-1L << 5L));
// 自定义初始化雪花算法
IdWorker.initSequence(workerId, dataCenterId);
}
/**
* 获取机器标识号
* @param serviceName 服务名称
*/
private Long getWorkerIdNum(String serviceName) {
// 实例化脚本对象
DefaultRedisScript<Long> lua = new DefaultRedisScript<>();
lua.setResultType(Long.class);
lua.setScriptText(script);
List<String> keys = List.of(idWorker, serviceName);
// 获取序列号
Long num = (Long)redisTemplate.execute(lua, keys, keys.size());
String key = String.join(":", keys) + ":" + num;
// -1 代表机器用完了,重试
if (num < 0){
log.error("目前Id已用完,请重新启动试试");
System.exit(0);
}
// 自动续期
this.autoExpire(key);
return num;
}
/**
* 自动续期
*/
private void autoExpire(String key) {
ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1,
new BasicThreadFactory.Builder().namingPattern("id_auto_expire-%d").daemon(true).build());
executorService.scheduleAtFixedRate(() -> {
redisTemplate.expire(key, 30, TimeUnit.SECONDS);
log.info("自动续期id成功:{}", key);
}, 0, 10, TimeUnit.SECONDS);
}
public String getServiceName() {
return serviceName;
}
public void setServiceName(String serviceName) {
this.serviceName = serviceName;
}
}
相关Maven
<!-- 依赖父类版本号 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
效果展示
每10秒自动续期