在开发中几乎所用的系统都会涉及到唯一单号的生成,通常分为两种:一种是有序的生成带有一定规则的单号,另一种是无序的随机生成唯一的单号。这里主要是介绍怎么才能在不同场景下生成有序带有一定规则的单号。
1.synchronized 同步获取单号
创建一个表来存储单号,使用唯一索引确保获得的每一个单号都是唯一的。
CREATE TABLE `generate_no` (
`tid` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`serial_no` varchar(50) NOT NULL COMMENT '流水号',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '最后修改时间',
PRIMARY KEY (`tid`),
UNIQUE KEY `uk_serial_no` (`serial_no`)
) ENGINE=InnoDB AUTO_INCREMENT=24 DEFAULT CHARSET=utf8mb4
先通过SQL获取单号,再把单号存到表里保证表里的单号是最新,加synchronized是防止并发的时候生成相同的单号,相同的单号在插入数据库触发唯一索引报错。
/**
* 获取单号
* @param prefix 前缀
* @return
*/
public synchronized String getSerialNo(String prefix) {
String dateStr = new SimpleDateFormat("yyMMdd").format(new Date());
String param = prefix + dateStr;
String serialNo = baseMapper.getSerialNo(param);
GenerateNo generateNo = new GenerateNo();
generateNo.setSerialNo(serialNo);
this.save(generateNo);
return serialNo;
}
获取单号SQL
SELECT CONCAT(#{param},LPAD(SUBSTR(IFNULL(MAX(serial_no),1),-4)+1,4,0))
FROM generate_no
WHERE serial_no LIKE CONCAT(#{param}, '%')
2.利用redis生成单号
没啥说的直接上代码。
@Autowired
private JedisCluster jedis;
/**
* 从redis获取最新单号
* 生成规则 前缀 + 日期 + 流水码(五位)
* @param prefix 前缀
* @return
*/
public String getAutoNumber(String prefix) {
String dateStr = new SimpleDateFormat("yyMMdd").format(new Date());
//redis 存储的 key
String autoKey = prefix + dateStr;
//校验key是否存在
if (jedis.exists(autoKey)) {
//key值不存在插入,如果存在即操作失败,不会覆盖value值
jedis.setnx(autoKey, String.valueOf(0));
}
//获取流水号
String autoNumber = String.valueOf(jedis.incr(autoKey));
DecimalFormat df = new DecimalFormat("00000");
//流水号不足5位补充成5位
autoNumber = df.format(Integer.parseInt(autoNumber));
//前缀 + 时间 + 流水码
String number = autoKey + autoNumber;
return number;
}
3.乐观锁获取单号
3.1 创建单号表
将单号拆分成三个字段(前缀+日期+流水号),避免表数据太多,所以表里面只存最大的单号。
CREATE TABLE `generate_no` (
`tid` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`prefix` varchar(100) NOT NULL COMMENT '单号前缀',
`serial_no` int(5) NOT NULL COMMENT '流水号',
`create_date` varchar(8) NOT NULL DEFAULT '' COMMENT '创建日期',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '最后修改时间',
PRIMARY KEY (`tid`),
UNIQUE KEY `uk_prefix_serial_no` (`prefix`,`serial_no`,`create_date`)
) ENGINE=InnoDB AUTO_INCREMENT=24 DEFAULT CHARSET=utf8mb4
3.2 获取单号的流程图
代码就不写了,画个流程图吧,按照流程图codeing就可以了,编码过程中注意异常的catch和版本控制。
4.mysql + redis + 分布式锁生 成单号
4.1 创建单号表
将单号拆分成三个字段(前缀+日期+流水号),避免表数据太多,所以表里面只存最大的单号。
CREATE TABLE `generate_no` (
`tid` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`prefix` varchar(100) NOT NULL COMMENT '单号前缀',
`serial_no` int(5) NOT NULL COMMENT '流水号',
`create_date` varchar(8) NOT NULL DEFAULT '' COMMENT '创建日期',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '最后修改时间',
PRIMARY KEY (`tid`),
UNIQUE KEY `uk_prefix_serial_no` (`prefix`,`serial_no`,`create_date`)
) ENGINE=InnoDB AUTO_INCREMENT=24 DEFAULT CHARSET=utf8mb4
4.2 获取单号流程图
任性不写代码,看流程图吧。
redis存储的单号使用List结构,通过lpop命令来弹出头部元素并返回来实现取单号。
5.总结
上面介绍了4种单号的生成方法,它们有个字的优缺点和使用场景。
第一种使用同步方法代码简洁,实现方便,但是效率低,只适合访问量较小的单体应用系统。
第二种使用redis生成单号代码简洁、实现方便、满足高并发,但是在用于redis数据存储在内存中,有key值丢失的风险。
第三种使用乐观锁生成单号是第一种的改良版,能满足高并发环境,由于每次生成单号都需要访问最少两次数据库,同样存在性能问题,适合访问量小的系统。
由于单独使用mysql或者redis都存在一些问题,也就产生了mysql结合redis的方案,也就是第四种方案,每次访问mysql都生成1000个单号存放在redis里面,将最大单号存放在mysql中,解决了mysql性能瓶颈和redis特殊情况下key值丢失问题,适用于大部分高并发、分布式系统使用。