Redis解决雪花算法(snowflake)dataId和workId的自动选择分发问题
1.简介
在分布式系统中,使用snowflake算法生成全局惟一标识符是很是方便的,可是snowflake算法有一个须要注意的地方,就是它拥有两个变量datacenterId和workerId,snowflake经过这两个变量来实现分布式系统下每一个服务生成不重复的ID, 可是这两个变量取值必须在 1 - 31之间(包括),也就是snowflake最多只能支持31*31=961个节点同时使用,且每一个节点的datacenterId和workerId不能重复,接下来我分享一个经过redis实现的datacenterId和workerId分发的算法,请指正:java
2.定义SnowflakeInitiator类,实现snowflake的机器码的分发:
/** * 雪花算法初始化器 * 初始化snowflake的dataCenterId和workerId * <p> * 1.系统启动时生成默认dataCenterId和workerId,并尝试做为key存储到redis * 2.若是存储成功,设置redis过时时间为24h,把当前dataCenterId和workerId传入snowflake * 3.若是存储失败workerId自加1,并判断workerId不大于31,重复1步骤 * 4.定义一个定时器,每隔24h刷新redis的过时时间为24h * * @CreatedBy: yangcan 2020/5/13 14:05 * @Description: */ @Component //@Configuration public class SnowflakeInitiator { /** * snowflake的dataCenterId和workerId */ public static SnowflakeVo snowflakeVo; private static String prefixRedisKey = "YC_SnowflakeRedisKey"; private static String snowflakeRedisKey; private static long LockExpire = 60 * 60 * 24; private static boolean stopTrying = false; @Autowired private RedisTemplate redisTemplate; public void init() throws InterruptedException { if (stopTrying) { System.out.println("snowflake强制结束生成key,key = " + JSON.toJSONString(snowflakeVo)); return; } if (tryInit()) { System.out.println("snowflake结束生成key,key = " + JSON.toJSONString(snowflakeVo)); return; } Thread.sleep(10); init(); } public boolean tryInit() { snowflakeVo = nextKey(snowflakeVo); snowflakeRedisKey = prefixRedisKey + "_" + snowflakeVo.getDataCenterId() + "_" + snowflakeVo.getWorkerId(); if (redisTemplate.hasKey(snowflakeRedisKey) == false) { if (redisTemplate.opsForValue().setIfAbsent(snowflakeRedisKey, 1, LockExpire, TimeUnit.SECONDS)) { System.out.println("成功抢占锁,Constants.snowflakeVo = " + JSON.toJSONString(snowflakeVo)); return true; } } return false; } /** * 生成下一组不重复的dataCenterId和workerId * * @return */ private SnowflakeVo nextKey(SnowflakeVo snowflakeVo) { if (snowflakeVo == null) { return new SnowflakeVo(1L, 1L); } if (snowflakeVo.getWorkerId() < 31) { // 若是workerId < 31 snowflakeVo.setWorkerId(snowflakeVo.getWorkerId() + 1); } else { // 若是workerId >= 31 if (snowflakeVo.getDataCenterId() < 31) { // 若是workerId >= 31 && dataCenterId < 31 snowflakeVo.setDataCenterId(snowflakeVo.getDataCenterId() + 1); snowflakeVo.setWorkerId(1L); } else { // 若是workerId >= 31 && dataCenterId >= 31 snowflakeVo.setDataCenterId(1L); snowflakeVo.setWorkerId(1L); stopTrying = true; } } return snowflakeVo; } /** * 从新设置过时时间,由定时任务调用 */ public void resetExpire() { redisTemplate.expire(snowflakeRedisKey, (LockExpire - 600), TimeUnit.SECONDS); System.out.println("YC 执行定时任务重置snowflakeRedisKey过时时间 resetExpire() redisKey = " + snowflakeRedisKey); } /** * 容器销毁时主动删除redis注册记录,此方法不适用于强制终止Spring容器的场景,只做为补充优化 */ public void destroy() { redisTemplate.delete(snowflakeRedisKey); System.out.println("YC destroy snowflakeRedisKey = " + redisKey); } @Data @NoArgsConstructor @AllArgsConstructor public class SnowflakeVo { private Long dataCenterId; private Long workerId; } }
3.定义定时任务,定时刷新redis的过时时间
@Component public class MySchedule { @Autowired private SnowflakeInitiator snowflakeInitiator; @Scheduled(fixedDelay = 1000 * 60 * 60 * 23) private void snowflakeInitiator_ResetExpire() { snowflakeInitiator.resetExpire(); } }
4.用@PreDestroy注解实现,当服务中止后自动删除redis记录
/** * 在服务强制kill的状况下不触发@PreDestroy,此方法只做为补充方法使用 */ @PreDestroy public void destroy() { System.out.println("YC destroy something start"); snowflakeInitiator.destroy(); System.out.println("YC destroy something end"); }
4.大体流程
- 1.系统启动时生成默认dataCenterId和workerId,并尝试做为key存储到redis
- 2.若是存储成功,设置redis过时时间为24h,把当前dataCenterId和workerId传入snowflake
- 3.若是存储失败workerId自加1,并判断workerId不大于31,重复1步骤
- 4.定义一个定时器,每隔24h刷新redis的过时时间为24h
5.缺陷
- 1.服务启动的时候就尝试链接redis获取机器码,会形成服务启动比平时慢5s左右(具体看电脑配置)
- 2.在tryInit()的过程当中,最多会重试961次(经过测试,重试1000次会延迟6s左右),也会形成服务启动慢
- 3.当重试961次(即全部机器码都被占用了),系统会默认返回机器码1-1(这是snowflake硬伤,没办法,只能从自己系统上优化)
- 4.当服务被强制kill掉时,@PreDestroy注解不会被触发,只能经过自己设置的过时日期(24h)等待过时(这个缺陷目前只想到了经过缩短过时日期优化)
- 5.此方法依赖于spring的Bean注入方式保证单例,若是经过new SnowflakeInitiator()的方式实例化就会失效(能够自行优化或写成单例默认)
- 6.不支持多线程调用,想多线程调用的本身优化
6.小结
每次用snowflake的时候都会有这方面的苦恼,此次提供的方式只是暂时解决了snowflake的问题,不过可能还有其余更优方式,作个调查:你们有没有尝试把ID生成模块独立出来作一个单独的服务,以供其余业务服务使用的?redis