• 【Redis】解决全局唯一 id 问题


    永远要记得坚持的意义

    一、全局唯一 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);
    //    }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    测试类代码:

    @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));
        }
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    使用全局唯一 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();
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
  • 相关阅读:
    Nacos支持https
    RFID警用装备管理系统-公安警用装备管理可视化系统
    AVM赛道研究:预计2024年渗透率突破50%!下一个破局点在哪儿?
    2023西南石油大学计算机考研信息汇总
    js dispatchEvent派发自定义事件
    win10&11安装MG-SOFT+MIB+Browser+v10b
    激活函数之ReLU, GeLU, SwiGLU
    Qt开发_调用OpenCV(3.4.7)设计完成人脸检测系统
    自动化工具
    【刷题篇】贪心算法(二)
  • 原文地址:https://blog.csdn.net/liuwanqing233333/article/details/128192748