基于Redis生成全局ID
方法一:预先生成订单号方案
该方案主要步骤如下:
预生成订单号并存入Redis
- 首先,可统计当天的订单数据量基于系统当前时间戳生成一批订单号。
- 将这些订单号按顺序推入Redis列表中。
- 每次需要生成订单号时,从Redis列表中
弹出
一个。
import redis.clients.jedis.Jedis;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* @author wstv
*/
public class OrderNumberGenerator {
private Jedis jedis;
public OrderNumberGenerator(Jedis jedis) {
this.jedis = jedis;
}
/**
* 提前生成一批订单号并放入Redis
* @param batchSize 批量生成的数量
*/
public void preGenerateOrderNumbers(int batchSize) {
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");
String currentDateStr = now.format(formatter);
for (int i = 0; i < batchSize; i++) {
String orderId = currentDateStr + String.format("%05d", i); // 前面加上系统时间戳,后面补足零
jedis.rpush("order_numbers", orderId);
}
}
/**
* 从Redis中取出一个订单号
* @return 订单号,若无则返回null
*/
public String getNextOrderNumber() {
String orderId = jedis.lpop("order_numbers");
if (orderId != null) {
return orderId;
} else {
// 当订单号不足时,重新生成一批
preGenerateOrderNumbers(1000); // 示例批量生成1000个
return getNextOrderNumber(); // 再次尝试获取
}
}
}
// 使用示例:
Jedis jedis = new Jedis("localhost"); // 初始化Jedis连接
OrderNumberGenerator generator = new OrderNumberGenerator(jedis);
generator.preGenerateOrderNumbers(1000); // 预先生成一批订单号
String orderNumber = generator.getNextOrderNumber(); // 获取一个订单号
System.out.println(orderNumber);
方法二:利用Redis自增特性
因为Redis是单线的,天生保证原子性,可以使用Redis的原子操作 INCR和INCRBY来实现
但是这种方式存在局限性,如果服务重启,如果没有持久化配置,可能会导致计数器从 0 开始,所以通常我们会设置 Redis 数据库持久化或者定期备份计数值。
优缺点
-
优点:
- 不依赖数据库,效率高,性能优越。
- 数字ID自然排序,有利于分页查询和排序操作。
-
缺点:
- 如果系统尚未集成Redis,则需额外引入组件,增加系统复杂度。
- 需要编码和配置工作较多。
加入每天从0开始的订单编号需求,我们将为每一天创建一个独立的Redis键,并使用INCR原子操作来递增订单号。下面是一个基础实现:
import redis.clients.jedis.Jedis;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public class RedisBasedOrderIdGenerator {
private static final String KEY_PREFIX = "daily_order_id_";
private final Jedis jedis;
private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
public RedisBasedOrderIdGenerator(Jedis jedis) {
this.jedis = jedis;
}
/**
* 生成一个新的订单号,格式为:日期(年月日)+ 当日自增ID(左补零至10位)
* @return 新的订单号
*/
public String generateNewOrderId() {
LocalDate today = LocalDate.now();
String key = KEY_PREFIX + today.format(dateFormatter);
// 设置Redis键的过期时间,比如让其在当天结束时自动删除,以便第二天从0开始
jedis.expire(key, calculateExpireSecondsForToday());
// 自增并获取新值
long autoIncrementId = jedis.incr(key);
// 将自增ID转换为字符串,并补足前导零
String orderId = today.format(dateFormatter) + String.format("%010d", autoIncrementId);
return orderId;
}
/**
* 根据当前日期计算Redis键的有效期秒数,使其在每天结束时过期
* @return 当天剩余的秒数
*/
private int calculateExpireSecondsForToday() {
LocalDate tomorrow = LocalDate.now().plusDays(1);
LocalDateTime endOfToday = tomorrow.atStartOfDay();
return (int) Duration.between(LocalDateTime.now(), endOfToday).getSeconds();
}
// 使用示例:
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost");
RedisBasedOrderIdGenerator generator = new RedisBasedOrderIdGenerator(jedis);
String orderId = generator.generateNewOrderId();
System.out.println(orderId);
}
}
注意:在Redis集群情况下,同样和Redis一样需要设置不同的增长步长,同时key一定要设置有效期
可以使用Redis集群来获取更高的吞吐量。
-
示例:假设有一个包含 5 台 Redis 的集群,初始值和步长分别为 1,2,3,4,5 和 5,各节点生成的 ID 如下:
-
A:1,6,11,16,21
-
B:2,7,12,17,22
-
C:3,8,13,18,23
-
D:4,9,14,19,24
-
E:5,10,15,20,25
比较适合使用Redis来生成每天从0开始的流水号。比如订单号=日期+当日自增长号。可以每天在Redis中生成一个Key,使用INCR进行累加。
如果生成的订单号超过自增增长的话,可以采用前缀+自增+并且设置有效期
方法三:Snowflake 算法(雪花算法)
Snowflake 算法是一种 Twitter 开源的分布式 ID 生成算法,它能生成一个64位的全局唯一ID,且具备良好的趋势递增和可读性。
通过 Redis 作为存储媒介,确保其中的部分序列号部分(sequence)在多节点下有序递增。
// 在 Redis 中维护序列号部分
long sequence = redis.incr("sequence");
long timestampBits = getCurrentTimestampInSnowflakeFormat();
long workerIdBits = getWorkerIdInSnowflakeFormat();
// 合成全局ID
long globalId = (timestampBits << timestampShift) | (workerIdBits << workerIdShift) | sequence;
方法四、分布式锁+自增ID
- 在多节点服务架构中,为避免多个服务实例同时修改同一计数器引发冲突,可使用 Redis 分布式锁(例如 RedLock 或
SETNX
加EXPIRE
)来确保同一时间仅有一个实例生成 ID:
try {
// 获取分布式锁
if (redis.setnx(lockKey, currentThreadId, expireTime)) {
long currentId = redis.incr("global_id_counter");
// 释放锁
redis.del(lockKey);
return currentId;
}
} catch (Exception e) {
// 锁获取失败处理
}