微服务应用(十四):基于RedisINCR生成递增分布式唯一ID

star2017 1年前 ⋅ 708 阅读

分布式发号器(ID生成器)可选方案和需要满足的特性,可参考 分布式微服务系列(十七):高性能分布式发号器(ID生成器)

本篇基于 Redis 的原子递增命令 INCR 实现 递增ID 的操作。经 200 并发线程测试 5 次,没有出现重复ID的情况。

Redis 命令

INCR

将对应键的数字值加一。如果键不存在,则在执行操作之前将其设置为0。如果键包含错误类型的值或包含不能表示为整数的字符串,则返回错误。此操作仅限于64位有符号整数

注意:这是一个字符串操作,因为 Redis 没有专用的整数类型。存储在键的字符串被解释为以10为基数的64位带符号整数,以执行操作。

Redis 以整数表示形式来存储整数,因此对于实际包含整数的字符串值,存储字符串整数的表示形式的不会有额外开销。

INCRBY

描述基本同上,但可以指定自增步长

Redis自增ID

添加依赖

Spring Boot 项目添加 spring-boot-starter-data-redis 依赖,Redis 客户端默认使用 Lettuce,需要添加 commons-pool2 依赖来使用连接池。

<!--redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.8.1</version>
</dependency>

配置连接

#==========Redis=========================
spring.redis.host=192.168.50.132
spring.redis.port=6379
spring.redis.database=1

配置序列化

/**
 * @desc: Redis 序列化配置
 */
@Configuration
public class RedisConfig {

    /**
     * 对象序列化
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        FastJsonRedisSerializer<Object> serializer = new FastJsonRedisSerializer<>(Object.class);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(serializer);
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(serializer);

        //开启事务支持
        redisTemplate.setEnableTransactionSupport(true);

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

Redis ID 自增实现

spring-data-redis包中提供了一个 RedisAtomicLong类,可以对数字中的Long类型进行原子性操作

RedisAtomicLong 实现基于 Redis 支持的原子操作。 将 Redis 原子increment/decrementwatch/multi/exec操作用于CAS操作。

/**
 * @desc: Redis ID 工厂
 */
@Component
public class RedisSequenceFactory {

    private static final String MEAL_NO_PREFIX = "conv:mealNo:";
    private static final String ORDER_NO_PREFIX = "conv:orderNo:";
    private static final String PAY_NO_PREFIX = "conv:payNo:";
    private static final String REFUND_NO_PREFIX = "conv:refundNo:";

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * @desc: 生成取餐号
     */
    public String generateMealNo() {
        LocalDate now = LocalDate.now();
        String yyyyMMdd = now.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        String key = MEAL_NO_PREFIX + yyyyMMdd;
        RedisAtomicLong counter = new RedisAtomicLong(key, redisTemplate.getConnectionFactory());
        //24小时后失效
        counter.expire(24, TimeUnit.HOURS);
        long no = counter.incrementAndGet();
        String noStr = String.format("%03d", no);
        return yyyyMMdd + noStr;
    }

    /**
     * @desc: 生成订单号
     */
    public String generateOrderNo(String prefix) {
        LocalDate now = LocalDate.now();
        String yyyyMMdd = now.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        String key = ORDER_NO_PREFIX + yyyyMMdd;
        RedisAtomicLong counter = new RedisAtomicLong(key, redisTemplate.getConnectionFactory());
        //24小时后失效
        counter.expire(24, TimeUnit.HOURS);
        long no = counter.incrementAndGet();
        String noStr = String.format("%03d", no);
        return prefix + yyyyMMdd + noStr;
    }

    /**
     * @desc: 生成支付单号
     * @param: [prefix]
     */
    public String generatePayNo(String prefix) {
        LocalDate day = LocalDate.now();
        String yyyyMMdd = day.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        String key = PAY_NO_PREFIX + yyyyMMdd;
        RedisAtomicLong counter = new RedisAtomicLong(key, redisTemplate.getConnectionFactory());
        //24小时后失效
        counter.expire(24, TimeUnit.HOURS);
        long no = counter.incrementAndGet();
        String noStr = String.format("%03d", no);

        LocalDateTime time = LocalDateTime.now();
        String yyyyMMddHHmmss = time.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
        return prefix + yyyyMMddHHmmss + noStr;
    }

    /**
     * @desc: 生成退款单号
     * @param: [prefix]
     */
    public String generateRefundNo(String prefix) {
        LocalDate day = LocalDate.now();
        String yyyyMMdd = day.format(DateTimeFormatter.ofPattern("yyyyMMdd"));

        String key = REFUND_NO_PREFIX + yyyyMMdd;
        RedisAtomicLong counter = new RedisAtomicLong(key, redisTemplate.getConnectionFactory());
        //24小时后失效
        counter.expire(24, TimeUnit.HOURS);
        long no = counter.incrementAndGet();
        String noStr = String.format("%03d", no);

        LocalDateTime time = LocalDateTime.now();
        String yyyyMMddHHmmss = time.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));

        return prefix + yyyyMMddHHmmss + noStr;
    }
}

集群环境

网上有些文章说 Redis 集群环境需要给各个的节点设置不同起始值,设置步长。--这些描述并不严谨的,甚至产生误导

如果 Redis 集群使用的是原生的方式搭建的,此集群不是简单的负载均衡方式的集群。

原生的方式搭建的集群,Redis Cluster 引入了 Hash Slot(哈希槽)的概念,集群中的每一个节点负责某一部分哈希槽,Key 所落的哈希槽是确定的,即每个 Key 在整个集群的所有主节点只会有一份数据,所以基于 Redis 集群实现的 自增ID 与单机实现的结果是一样的,也就不存在网上某些文章说的需要给 Redis 各个节点设置不同起始值 和 设置步长。

如果 Redis 集群使用的不是原生提供的方式搭建,那就另说。所以里必须要明确说明搭建集群的方式

其它参考

  1. Redis Command INCR
  2. 高并发环境下,Redisson实现redis分布式锁
  3. Redis原子计数器incr,防止并发请求
  4. Redis的原子自增性
  5. 分布式ID之Redis集群实现的分布式ID
更多内容请访问:IT源点

相关文章推荐

全部评论: 0

    我有话说: