Redis实现全局唯一id(时间戳+序列号实现)

一.使用原因

1.全局ID生成器:是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性:

唯一性  高可用  高性能  递增性  安全性

2.如果使用数据库自增ID就存在一些问题:

  • id的规律性太明显

  • 受单表数据量的限制

3.业务方面:

随着我们规模越来越大,mysql的单表的容量不宜超过500W,数据量过大之后,我们要进行拆库拆表,但拆分表了之后,他们从逻辑上讲他们是同一张表,所以他们的id是不能一样的, 于是乎我们需要保证id的唯一性。

4.如果使用redis来实现全局ID,我们可以不直接使用Redis自增的数值,而是拼接一些其它信息:

通过拼接时间戳和序列号来组成唯一ID

时间戳:31bit,以秒为单位,可以使用69年

序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID

二.方法代码

代码:

package com.hmdp.utils;

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;

/**
 * 生产redis全局唯一id
 */
@Component
public class RedisIdWorker {
    /**
     * 开始时间戳
     */
    private static  final  long BEGIN_TIMESTAMP=1709251201L;

    /**
     * 序列号的位数
     */
    private static final int COUNT_BITS=32;

    @Resource
    private StringRedisTemplate stringRedisTemplate;
    //业务前缀
    public long nextId(String keyPrefix){
        //1.生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long tempTimes=nowSecond-BEGIN_TIMESTAMP;
        //2.生产序列号
                //2.1获取当前日期,精确到天(一直用一个key仍有可能出现超过上线,因此添加每日的日期去解决该问题,也方便于统计每年/月/日的总数据个数)
        String data=now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        //2.2自增长
        Long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + data);
        //3.拼接并返回
        return tempTimes<<COUNT_BITS|count;
    }

    public static void main(String[] args) {
        LocalDateTime time = LocalDateTime.of(2024, 3, 1, 0, 0, 0);
        long second = time.toEpochSecond(ZoneOffset.UTC);
        System.out.println("second = " + second);
    }
}

三.测试代码

测试代码:

    @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("begin = " + begin);
        System.out.println("end = " + end);
    }

测试结果

四.原理

  1. 对于多线程并发调用的情况,多个线程同时执行 nextId 方法时,由于 stringRedisTemplate.opsForValue().increment 操作是原子的,保证了同一时刻只有一个线程能够自增 count。即使多个线程同时调用 increment 方法,Redis 会保证最终结果的一致性,每个线程得到的 count 值是唯一的。

  2. 由于每个线程调用 nextId 方法时的 data 是通过时间戳获取的,而在同一秒内,多个线程生成的时间戳是相同的。这就保证了多个线程生成的 ID 在高位的时间戳部分是相同的,而在低位的序列号部分是不同的。

因此共享的 stringRedisTemplate 实例的原子自增操作,以及时间戳和序列号的组合,保证了在多线程并发调用时,每一个返回的 ID 都是不同的。

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,下面是ThinkPHP6实现Redis连接池以及Redis队列的详细代码实现。 首先,在ThinkPHP6中使用Redis需要安装`topthink/think-redis`扩展,可以通过以下命令进行安装: ``` composer require topthink/think-redis ``` 接下来,我们需要在项目的配置文件中配置Redis连接信息,可以在`config/database.php`文件中添加以下代码: ```php 'redis' => [ 'type' => 'redis', 'hostname' => '127.0.0.1', 'password' => '', 'port' => 6379, 'select' => 0, 'timeout' => 0, 'prefix' => '', 'persistent' => true, 'pool' => [ 'min_connections' => 1, 'max_connections' => 10, 'wait_timeout' => 3, 'max_idle_time' => 60, ], ], ``` 配置项说明: - `type`:数据库类型,这里填写`redis`。 - `hostname`:Redis主机地址。 - `password`:Redis密码,如果没有设置密码可以不填写。 - `port`:Redis端口号,默认为6379。 - `select`:选择的数据库,默认为0。 - `timeout`:连接Redis的超时时间,默认为0表示不限制。 - `prefix`:设置的键名前缀,默认为空。 - `persistent`:是否使用持久化连接,默认为true。 - `pool`:配置连接池信息,包括最小连接数、最大连接数、等待超时时间和最大空闲时间。 接下来,我们可以通过以下代码获取Redis连接并进行操作: ```php use think\facade\Cache; // 获取Redis连接 $redis = Cache::store('redis')->handler(); // 设置键值对 $redis->set('name', 'Tom'); // 获取键值对 $name = $redis->get('name'); echo $name; ``` 以上代码中,我们使用了ThinkPHP6的缓存门面`think\facade\Cache`来获取Redis连接,通过`store`方法指定使用`redis`缓存驱动,再通过`handler`方法获取Redis连接。 接下来,我们来实现Redis队列功能,具体的代码如下: ```php use think\queue\Job; use think\facade\Cache; // 定义任务处理类 class TestJob { public function fire(Job $job, $data) { // 获取Redis连接 $redis = Cache::store('redis')->handler(); // 从队列中取出任务数据 $name = $data['name']; // 进行任务处理 // ... // 任务处理完成后删除任务 $job->delete(); } } // 将任务加入队列 $jobHandlerClassName = 'TestJob'; // 任务处理类名 $jobData = ['name' => 'Tom']; // 任务数据 $queueName = 'test_queue'; // 队列名称 $delay = 0; // 延迟时间,默认为0 \think\Queue::later($delay, $jobHandlerClassName, $jobData, $queueName); ``` 以上代码中,我们首先定义了一个任务处理类`TestJob`,它实现了`fire`方法来处理任务。在`fire`方法中,我们首先获取Redis连接,然后从队列中取出任务数据,进行任务处理,并最终删除任务。 接下来,我们将任务加入队列。在代码中,我们使用了`think\Queue`门面的`later`方法来将任务加入队列,指定了任务处理类名、任务数据、队列名称和延迟时间(默认为0表示不延迟)。 以上就是ThinkPHP6实现Redis连接池和Redis队列的详细代码实现

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值