永远要记得坚持的意义
一、全局唯一 id 场景
概念: 以订单表的 id 为例
使用自增 id 会产生的问题:
- id 的规律性太明显,容易让用户猜测到一些信息
- 受表单数据量的限制 —— 分布式存储时,会产生问题 (自增长,容易产生 id 重复)
因此,我们需要定义全局 id
二、全局 id 生成器
全局 id 生成器的特性:
分布式系统下用来生成全局唯一 id 的工具,其特性有:
- 唯一性
- 高可用
- 高性能
- 递增性
- 安全性
全局唯一 id 生成的实现:
- UUID
16进制字符串,不单调递增,jdk自带的 - Redis 自增
- snowflake 算法
对时钟依赖非常高 - 数据库自增策略
数据库单独设置一个自增表,获取自增,实现唯一效果 (单调自增的数据库版,性能没有 Redis 自增好)
我们本文主要介绍 使用 Redis 作为全局唯一 id 生成器
我们不直接使用 redis 自增的数值,而是拼接一些其他信息,主要拼接的信息如下:
符号位 + 时间戳 + 序列号
三、技术实现
封装好的 Redis 生成唯一 id 的工具类如下:
@Component
public class RedisIdWorker {
private static final long BEGIN_TIME = 1670198400;
private StringRedisTemplate stringRedisTemplate;
private RedisIdWorker(StringRedisTemplate stringRedisTemplate){
this.stringRedisTemplate = stringRedisTemplate;
}
public long nextId(String keyPrefix){
// 1. 生成时间戳
LocalDateTime localDateTime = LocalDateTime.now();
long nowSecond = localDateTime.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowSecond - BEGIN_TIME;
// 2. 生成序列号 —— 防止超过上限(Redis 自增的上限 2 的64 次幂) 解决方式:再拼接一个日期字符串
// 2.1 获取当前日期
String day = localDateTime.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + day);
// 3. 拼接并返回
return timestamp<<32 | count; // 前 32 位为时间戳 ,后 32 位为 序列号(即 count)
}
// 生成时间戳开始时间 —— 我这里是 2022 / 12/ 5 日开始
// public static void main(String[] args) {
// LocalDateTime time = LocalDateTime.of(2022, 12, 5, 0, 0, 0);
// long second = time.toEpochSecond(ZoneOffset.UTC);
// System.out.println(second);
// }
}
测试类代码:
@Slf4j
@SpringBootTest
class HmDianPingApplicationTests {
@Autowired
private RedisIdWorker redisIdWorker;
// 500 线程的线程池子
private ExecutorService es = Executors.newFixedThreadPool(500);
@Test
public void testIdWorker() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(200);
// 每个线程生成 100 个 id
Runnable task = () -> {
for (int i=0; i<5; i++) {
long id = redisIdWorker.nextId("order");
log.info("id=" + id);
}
latch.countDown();
};
long begin = System.currentTimeMillis();
for (int i=0; i<200; i++){
es.submit(task);
}
latch.await();
long end = System.currentTimeMillis();
log.info("time=" + (end - begin));
}
}
使用全局唯一 id 解决秒杀业务
@Resource
private RedisIdWorker redisIdWorker;
@Resource
private ISeckillVoucherService seckillVoucherService;
@Override
@Transactional
public Result seckillVoucher(Long voucherId) {
// 1. 查询优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
// 2. 判断秒杀是否开始
if(voucher.getBeginTime().isAfter(LocalDateTime.now())){
return Result.fail("秒杀还未开始");
}
// 3. 判断秒杀是否已经结束
if(voucher.getEndTime().isBefore(LocalDateTime.now())){
return Result.fail("秒杀已经结束");
}
// 4. 判断库存是否充足
if(voucher.getStock() < 1){
return Result.fail("库存不足");
}
// 5. 扣除库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId)
.update();
if (!success){
return Result.fail("库存不足");
}
// 6. 创建订单
VoucherOrder voucherOrder = new VoucherOrder();
// 订单的参数 —— 订单 id \ 代金券 id \ 用户 id
// 生成唯一 id
long id = redisIdWorker.nextId("order");
voucherOrder.setId(id);
voucherOrder.setVoucherId(voucherId);
voucherOrder.setUserId(1L);
save(voucherOrder);
return Result.ok();
}