如何按日期生成订单号
需求描述
按日期生成订单号,格式为 2024010100001
,其中 20240101
是当天日期, 00001
是连续递增的序号,第二天从 00001
重新开始递增,生成的订单号不能重复。
实现方案
考虑到应用可能因为升级等原因重启,应用重启后生成的订单号不能和之前的重复,就不考虑直接在内存中生成订单号。
方案一:通过 Redis 实现
将当天日期字符串作为 Redis String 的 key,使用 Redis String 类型的 incr 命令可以对值做自增操作。
使用 Redis 实现有以下好处:
- Redis 的命令执行是单线程,不需要考虑并发可能造成订单号重复的问题。
- Redis 可以对过期的 key 可以自动清理。
- 相比于数据库,Redis 生成订单号的性能更好。
Java 实现代码如下:
@Service
public class OrderNumServiceImpl implements OrderNumService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public String createOrderNum() {
String key = new SimpleDateFormat("yyyyMMdd").format(new Date());
Long seqNum = stringRedisTemplate.opsForValue().increment(key);
if (seqNum == 1) {
stringRedisTemplate.expire(key, calcTodayRemainSeconds(), TimeUnit.SECONDS);
}
return key + String.format("%05", seqNum);
}
private long calcTodayRemainSeconds() {
Date date = new Date();
LocalDateTime current = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
LocalDateTime target = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault())
.plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0);
return ChronoUnit.SECONDS.between(current, target);
}
}
方案二: 通过数据库实现
如果应用程序只部署了数据库,没有部署 Redis,认为只为生成订单号部署 Redis 没有必要,也可以使用数据库实现。
创建数据表:
CREATE TABLE `order_num` (
`id` int NOT NULL AUTO_INCREMENT,
`key` varchar(64),
`seq_num` int,
PRIMARY KEY (`id`),
UNIQUE INDEX `unique_key_idx` (`key`)
)
使用数据库生成订单号就要考虑并发请求的场景,避免生成的订单号重复。
如果应用是单实例部署的,可以在应用程序中对生成订单号的代码加锁(例如 Java 中的 synchronized )。
如果应用是多实例部署的,就需要在数据库加锁。当天记录不存在时需要先在数据库中插入当天的记录,要考虑并发场景可能创建多条记录,可以在数据表中将key字段设置为唯一索引。当天记录更新时对数据库加锁有两种方式:
-
乐观锁
对数据表增加 version 字段,更新记录时同时加上 where version={查询出来的 version 字段值},如果更新失败说明有其它线程同时更新了此条记录,需要增加重试机制,例如重试3次。
-
悲观锁
查询当天记录时使用如下 SQL 将记录锁定
SELECT * FROM `order_num` WHERE `key` = ? FOR UPDATE
然后对 seq_num 字段加1后更新
UPDATE `order_num` SET `seq_num` = `seq_num` + 1 WHERE `key` = ?
总结
可以看出,使用数据库方案要考虑和处理的情况比 Redis 复杂,因此尽量考虑使用 Redis 方案。