分布式发号器(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/decrement
和watch/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 集群使用的不是原生提供的方式搭建,那就另说。所以里必须要明确说明搭建集群的方式。
其它参考
注意:本文归作者所有,未经作者允许,不得转载