微信公众号访问地址:基于Redis实现全局唯一Id
推荐文章:
1、使用原生Redis命令实现分布式锁
2、为什么引入Redisson分布式锁?
3、SpringBoot整合多数据源,并支持动态新增与切换(详细教程)
4、SpringBoot统一标准响应格式及异常处理
5、SpringBoot用线程池ThreadPoolTaskExecutor异步处理百万级数据
一、简介
使用数据库自增ID就存在一些问题:
1、受单表自增数量的限制;
原因:mysql单表的容量不宜超过500W条,数据量过大之后,就需要进行拆库拆表,但拆分表之后,它们从逻辑上讲仍然是同一张表,所以他们的id是不能一样的(不同表,若使用自增ID,是可能一样的),所以随着我们的业务数据越来越大,我们需要保证id的唯一性。
2、id的规律性太明显。
原因:自增id具有太明显的规则,用户或者说商业对手很容易猜测出来一些敏感信息,例如:在一天时间内,我们卖出了多少单,这明显不合适。
二、全局唯一ID生成策略
一般要满足下列特性:
为了增加ID的安全性,可以不直接使用Redis自增的数值,而是拼接一些其它信息:
组成说明:
1、符号位:1bit,永远为0;
2、时间戳(31 Bit):31bit,以秒为单位,可以使用69年;
3、序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID。
三、基于Redis实现全局唯一Id案例
原理:基于Redis 的INCR 命令生成分布式全局唯一id。INCR 命令主要有以下2个特征:
1、具备了“INCR AND GET”的原子操作,即:增加并返回结果的原子操作。这个原子性很方便我们实现获取ID。
2、Redis是单进程单线程架构,INCR命令不会出现id重复。
3.1、构建RedisIdUtils类
/**
* 功能描述:使用redis生成全局唯一ID
* @Author: yyalin
* @CreateDate: 2023/8/13 11:35
*/
@Component
public class RedisIdUtils {
//预生成开始时间戳
private static final long BEGIN_TIMESTAMP = 1640995200L;
//序列号的位数
private static final int COUNT_BITS = 32;
//redis提供的字符串
private StringRedisTemplate stringRedisTemplate;
//有参构造函数
public RedisIdUtils(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 功能描述:根据keyPrefix前缀生成对应业务的全局唯一ID
* @MethodName: nextId
* @MethodParam: [keyPrefix:使用前缀来区分不同的业务]
* @Return: long
* @Author: yyalin
* @CreateDate: 2023/8/13 12:20
*/
public long nextId(String keyPrefix) {
// 1.生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowSecond - BEGIN_TIMESTAMP;
// 2.生成序列号
// 2.1.获取当前日期,精确到天
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
// 2.2.自增长 icr:order:2023:08:13
long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
// 3.拼接并返回 timestamp << COUNT_BITS :向左移动32位
//原本时间戳在低位上,通过向左移动32位,变位到高位存储,低32位都是0,然后与自增序列按位操作
//形成低32位为序列号。
return timestamp << COUNT_BITS | count;
}
}
3.2、构建多线程测试类
**
* @Description: TODO:测试RedisIdUtils
* @Author: yyalin
* @CreateDate: 2023/8/13 12:27
* @Version: V1.0
*/
@Service
@Slf4j
public class TestRedisIdUtils {
@Autowired
private RedisIdUtils redisIdUtils;
//使用自定义的线程池
private ExecutorService executorService = Executors.newFixedThreadPool(500);
/**
* 功能描述:使用多线程测试生成40000条数据耗时
* @MethodName: testIdWorker
* @MethodParam: [nums]
* @Return: void
* @Author: yyalin
* @CreateDate: 2023/8/13 12:36
*/
public void testIdWorker(int nums) throws InterruptedException {
//同步协调在多线程的等待于唤醒问题 分线程全部走完之后,主线程再走
CountDownLatch latch = new CountDownLatch(nums);
Runnable task = () -> {
for (int i = 0; i < 100; i++) {
long id = redisIdUtils.nextId("order");
System.out.println("id = " + id);
}
latch.countDown();
};
long begin = System.currentTimeMillis();
for (int i = 0; i < nums; i++) {
executorService.submit(task);
}
//阻塞方法 让main线程阻塞
latch.await();
long end = System.currentTimeMillis();
log.info("生成"+nums*100+"条id共计耗时(毫秒) = " + (end - begin));
}
}
3.3、测试结果
序列 | 线程数 | 条数 | 耗时(秒) |
1 | 500 | 30000 | 0.781 |
2 | 500 | 40000 | 0.993 |
3 | 500 | 50000 | 1.164 |
总结:从测试结果不难看出,基于redis实现全局唯一ID,性能还是非常高的,并且耗时非常短的。
更多优秀文章,请关注个人微信公众号或搜索“程序猿小杨”查阅。
参考文章:
https://blog.csdn.net/weixin_43811057/article/details/130798254