分布式系统内集成雪花算法的实践
前言
之前业务系统一致使用UUID来作为生成唯一标识,由于是分布式业务并且是多机器多节点,所以可能会存在重复情况,之前的业务也只是用捕获入库失败异常来for循环三次去实现减少失败情况,但是小概率还是会存在失败,尽管非常小概率…所以对代码进行优化,使用雪花算法进行修改。
如下开始雪花算法的实现
一、什么是雪花算法?
雪花算法是64位的二进制组成,展示如下:
具体算法实现可以百度,不讲算法。我们能自定义的就只剩下机器id和服务id号。
二、实现
1.思路
一般在分布式环境下,以应用名+机器号存入redis,服务初始化时,参照Redission实现分布式锁一样,开一个线程做完看门狗来续期,用lua脚本获取一个唯一且有效的序列号,设置过期时间为30秒
获取序列化之后,开启一个守护线程,每20秒延长过期时间为30秒,机器id就一直占用,服务关闭,这个Id会自动释放
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @Classname SnowFlakeIDWorker
* @Description 分布式雪花算法workerId维护
* @Version 1.0.0
* @Date 2023/2/17 10:47
* @Created by WFX1024
*/
@Slf4j
@Component
public class SnowFlakeIDWorker {
/**
* key标识
*/
private final String idWorker = "SnowFlakeWorkerId";
/**
* lua 脚本
*/
private final String script =
"redis.replicate_commands();local now = redis.call('TIME')[1]\n" +
"local idWordsKey = KEYS[1]\n" +
"local serviceName = KEYS[2]\n" +
"local sp = ':'\n" +
"for i = 0, 100 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 服务名称
*/
public Long getWorkerIdNum(String serviceName) {
// 实例化脚本对象
DefaultRedisScript<Long> lua = new DefaultRedisScript<>();
lua.setResultType(Long.class);
lua.setScriptText(script);
List<String> keys = Lists.newArrayList(idWorker, serviceName);
// 获取序列号
Long num = (Long)redisTemplate.execute(lua, keys, keys.size());
String key = String.join(":", keys) + ":" + num;
// -1 代表workerId用完了
if (num < 0){
log.error("目前Id已用完,num:{}",num);
throw new RuntimeException("workerId超出最大数量");
}
// 自动续期
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("SnowFlakeIDWorker-自动续期id成功:{}", key);
}, 0, 20, TimeUnit.SECONDS);
}
}
2.使用集成MybatisPlus的雪花算法实现
代码如下(示例):
/**
* @Classname MybatisPlusConfig
* @Description mybatisplus配置
* @Version 1.0.0
* @Date 2023/2/17 10:47
* @Created by WFX1024
*/
@EnableTransactionManagement
@Configuration
@MapperScan(basePackages = { "com.*.*.mapper"})
public class MybatisPlusConfig {
/**
* 分页插件,自动识别数据库类型 多租户,请参考官网【插件扩展】
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
return new PaginationInterceptor();
}
@Autowired
private SnowFlakeIDWorker snowFlakeIDWorker;
/**
* 初始化雪花ID
*/
@PostConstruct
public void initMybatisPlus(){
long num = snowFlakeIDWorker.getWorkerIdNum("activity");
// 获取前5位
long workerId = num >> 5;
// 获取后5位
long dataCenterId = num & (~(-1L << 5L));
// 自定义初始化雪花算法
IdWorker.initSequence(workerId, dataCenterId);
}
}
配置在初始化配置时候初始化雪花算法。
maven配置
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
总结
本次主要实现了使用Lua脚本+redis实现作为机器号存储,自动过期机制维护机器号,从而实现不用单独的分布式id服务进行分布式id生成。