基于 Redis INCR 生成递增分布式唯一ID

分布式发号器(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 依赖来使用连接池。

1
2
3
4
5
6
7
8
9
10
<!--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>

配置连接

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

配置序列化

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
/**
* @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操作。

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
/**
* @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
作者

光星

发布于

2020-08-19

更新于

2022-07-12

许可协议

评论