基于Redis自增实现全局ID生成器(详解)

 本博客为个人学习笔记,学习网站与详细见:黑马程序员Redis入门到实战 P48 - P49 

目录

全局ID生成器介绍

基于Redis自增实现全局ID

实现代码

功能测试


全局ID生成器介绍

背景介绍
当用户在抢购商品时,会生成订单并保存到数据库的某一张表中,而订单id如果使用数据库表的自增ID就会存在一些问题:
1. id的规律性太明显
2. 受单表数据量的限制

基于使用数据库自增ID带来的两个问题,我们来做场景分析:
1. 场景分析一:如果订单id具有太明显的规则,用户或者说商业对手很容易猜测出我们的一些敏感信息,比如商城在一天时间内,卖出了多少单,这明显不合适。
2. 场景分析二:MySQL的单表容量不宜超过500万条记录。随着我们商城规模的扩大,数据量增长到一定程度后,我们需要进行数据库拆分和表拆分。拆分后,这些表在逻辑上仍然属于同一张表,因此它们之间的数据ID不能相同。因此,我们必须确保全局ID的唯一性。

全局ID生成器
全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性:
1. 唯一性
2. 高性能
3. 高可用
4. 递增性
5. 安全性


基于Redis自增实现全局ID

全局ID组成结构图:

序列号:由于Redis的自增操作是原子性的,保证了在并发情况下生成ID的唯一性,避免了传统数据库中的锁竞争和性能瓶颈。因此我们可以利用Redis的自增原子性,让序列号由Redis自增的数值组成,因此我们确保了全局ID序列号的唯一性,从而确保了整个全局ID的唯一性。

同时,我们还需要考虑一个问题,我们利用Redis自增实现全局ID,但如果我们只设置一个Key值,随着业务的日积月累,自增值将会达到上限。为避免这种情况发生,我们需要设置不同的Key值,于是我们决定用年月日的格式 yyyy:MM:dd 来添加到Key值的前缀当中,因此一个Key值的自增量不再是用来表示所有时间的业务量,而只是用来表示某年某月某天的业务量,而一天的业务量是不可能超过 2^32 (几十亿) 这么大的数值的,我们从而确保了Key值不会达到上限。

而这种做法也方便了我们对业务数据的统计,当我们想查询一年中的业务量时,我们只需要查询前缀为 yyyy 的Key值自增量即可,如果我们想查询某年某月的业务量时,我们只需要查询前缀为 yyyy:MM 的Key值自增量即可。

时间戳:为了增加全局ID的安全性,我们并能不直接把Redis的自增值(序列号)当作全局ID,而是应该在此基础上拼接一些其它信息,我们可以先设置某一个时间的时间戳作为参照时间戳,如2000年1月1日0时0分0秒,之后每当用户下单,我们可以获取下单时间的时间戳,再与参照时间戳做差,得到的差值用来组成全局ID的时间戳这一部分。(显然,我们全局ID设置的时间戳只有32位,因此我们需要确保差值是在2^32大小内,而2^32秒相当于136年的时间,因此是妥妥够用的,或者我们也可以选择对参照时间差进行调整来确保差值不会超过2^32)


实现代码

全局ID生成器代码如下

@Component
public class RedisIdWorker {

    private static final long BEGIN_TIMESTAMP = 1640995200L;
    private static final long COUNT_BITS = 32;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    public long nextId(String KeyPrefix) {
        // 1.生成全局ID时间戳部分
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timestamp = nowSecond - BEGIN_TIMESTAMP;

        // 2.生成全局ID序列号部分
        // 2.1获取当前日期,精确到天
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        // 2.2获取自增长值
        Long count = stringRedisTemplate.opsForValue().increment("icr:" + KeyPrefix + ":" + date);

        // 3.拼接时间戳和序列号并返回
        return timestamp << COUNT_BITS | count;
    }

    // 用于计算20220101时间戳给BEGIN_TIMESTAMP赋值
    public static void main(String[] args) {
        LocalDateTime time = LocalDateTime.of(2022, 1, 1, 0, 0, 0);
        long second = time.toEpochSecond(ZoneOffset.UTC);
        System.out.println("second = " + second);
    }

}

功能测试

编写测试类对全局ID生成器进行测试,在测试类中开启300个线程,让每个线程生成100个全局ID,最终将生成30000个ID,Redis中对应的key自增长将会达到30000。

@SpringBootTest
class HmDianPingApplicationTests {

    @Resource
    private RedisIdWorker redisIdWorker;

    //建立线程池,赋予500个线程
    private ExecutorService es = Executors.newFixedThreadPool(500);

    @Test
    void testIdWorker() throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(300);
        Runnable task = () -> {
            for (int i = 0; i < 100; i++) {
                long id = redisIdWorker.nextId("order");
                System.out.println("id = " + id);
            }
            latch.countDown();
        };
        long begin = System.currentTimeMillis();
        for (int i = 0; i < 300; i++) {
            es.submit(task);
        }
        latch.await();
        long end = System.currentTimeMillis();
        System.out.println("time = " + (end-begin));
    }

}

成功运行测试类后,在RESP中查询对应的key值,如下图所示,key值为30000,和预期相同。


知识小贴士:关于CountDownLatch

CountDownLatch名为信号枪:主要的作用是同步协调在多线程的等待于唤醒问题。

如果没有CountDownLatch ,由于程序是异步的,当异步程序没有执行完时,主线程就已经执行完了,这将导致计时失效。我们期望的是分线程全部走完之后,主线程再走,所以我们需要使用到CountDownLatch。

CountDownLatch 中有两个重要的方法

1、countDown

2、await

await 方法是阻塞方法,我们担心分线程没有执行完时,main线程就先执行,所以使用await可以让main线程阻塞,那么什么时候main线程不再阻塞呢?当CountDownLatch 内部维护的 变量变为0时,就不再阻塞,直接放行,那么什么时候CountDownLatch 维护的变量变为0 呢?我们只需要调用一次countDown ,内部变量就减少1,我们让分线程和变量绑定, 执行完一个分线程就减少一个变量,当分线程全部走完,CountDownLatch 维护的变量就是0,此时await就不再阻塞,统计出来的时间也就是所有分线程执行完后的时间。

  • 21
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要使用Redis生成自增ID,可以借助Redis的INCR命令。INCR命令用于将一个键的值增加1,并返回增加后的值。你可以在你的代码中使用INCR命令来生成自增ID。下面是一个示例代码片段: ``` @ApiOperation("根据key生成自增Id") @RequestMapping(value = "/getId/{key}", method = RequestMethod.GET) public GenericResponse<Long> getId(@PathVariable("key") String key) { logger.info("生成Id中"); Long id = redisTemplate.opsForValue().increment(key, 1); return new GenericResponse<>(id); } ``` 在这个示例中,我们使用了Spring Data Redis提供的`redisTemplate`来执行INCR命令,通过操作键的值来实现自增。当调用这个接口时,每次都会生成一个自增ID,并返回给用户。你可以根据实际需求对生成的ID进行格式化,比如添加日期前缀或补0等操作。 需要注意的是,为了使用Redis生成自增ID,你需要先确保已经正确地配置了Redis服务器,并在代码中引入相关的依赖。此外,你还可以使用Redis的其他功能,比如设置过期时间等,来满足自己的业务需求。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [REDIS生成分布式环境下自增ID](https://blog.csdn.net/yucaifu1989/article/details/122132257)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [毕设项目:基于SpringBoot+MyBatis-Plus 前后端分离的影院选座购票系统.zip](https://download.csdn.net/download/dd_vision/88222307)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值