分布式发号器(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
| <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
| 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
|
@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/decrement
和watch/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
|
@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;
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()); counter.expire(24, TimeUnit.HOURS); long no = counter.incrementAndGet(); String noStr = String.format("%03d", no); return yyyyMMdd + noStr; }
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()); counter.expire(24, TimeUnit.HOURS); long no = counter.incrementAndGet(); String noStr = String.format("%03d", no); return prefix + yyyyMMdd + noStr; }
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()); 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; }
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()); 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 集群使用的不是原生提供的方式搭建,那就另说。所以里必须要明确说明搭建集群的方式。
其它参考
- Redis Command INCR
- 高并发环境下,Redisson实现redis分布式锁
- Redis原子计数器incr,防止并发请求
- Redis的原子自增性
- 分布式ID之Redis集群实现的分布式ID