持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第23天,点击查看活动详情
背景
在我们的日常开发中,经常会遇到需要生成订单号的需求,比如商品编号、交易单号、快递单号等等,下面就来介绍几种常见的序列号生成方式。
数据库生成序列号
- 创建数据表
-- ----------------------------
-- Table structure for sequence
-- ----------------------------
DROP TABLE IF EXISTS `sequence`;
CREATE TABLE `sequence` (
`key` varchar(64) COLLATE utf8mb4_bin NOT NULL COMMENT '主键',
`current_no` int NOT NULL COMMENT '序列号',
`step` int NOT NULL COMMENT '自增步长',
PRIMARY KEY (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
1.在业务系统中,可能会存在多个业务需要生成序列号的需求,所以我们在创建表结构的时候,需要区分出不同业务对应的序列号,所以才创建
key
这个字段;2.
current_no
字段值就是我们需要的目标值;3.
step
是我们自增的步长,默认是1;
- 编写存储过程
CREATE DEFINER=`root`@`%` PROCEDURE `get_sequence_proc`(IN sequence_key varchar(64))
BEGIN
# 开启事务
START TRANSACTION;
# 如果主键不存在,就插入一条新数据;如果主键存储,就更新current_no=current_no+step,超过999999重置为1
INSERT INTO sequence VALUES (sequence_key,1,1) ON DUPLICATE KEY UPDATE current_no=IF(current_no+step>999999,1,current_no+step);
# 查询current_no
SELECT current_no FROM sequence WHERE `key`=sequence_key;
# 提交事务
COMMIT;
END
1.为了方便起见,我们直接创建一个存储过程,这样在我们的java应用端只需要直接调用即可;
2.存储过程名称为
get_sequence_proc
,输入参数为sequence_key
,字符串类型;3.为了保证生成序列号的原子性,我们需要开启事务;
4.使用
INSERT INTO ON DUPLICATE KEY UPDATE
语法特性,如果数据表中不存在对应的key
,那么就插入一条新记录,如果已经存在对应的key
,那么就更新指定的current_no
字段值;5.我们要避免
current_no
字段值无限制地自增,所以需要设置一个上限值;6.最后我们查询出自增后的
current_no
字段值并返回;
- Mybatis调用存储过程
SequenceMapper.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.awesomeaccount.dao.mapper.SequenceMapper">
<select id="nextSequence" statementType="CALLABLE" resultType="long">
CALL get_sequence_proc(#{sequenceKey,mode=IN,jdbcType=VARCHAR})
</select>
</mapper>
1.和平常的select一样,需要注意的是,
statementType="CALLABLE"
才表示调用存储过程;2.SQL语法就是直接
CALL 存储过程名称(输入参数)
即可;3.因为最后的返回值是
current_no
,所以我们在java应用端用long类型接收;
import org.apache.ibatis.annotations.Param;
/**
* @author zouwei
* @className WalletEnhanceMapper
* @date: 2022/9/27 21:33
* @description:
*/
public interface SequenceMapper {
Long nextSequence(@Param("sequenceKey") String sequenceKey);
}
这样我们就可以愉快地在业务逻辑中使用序列号生成器了。
Redis生成序列号
在redis中生成序列号,我们最先想到的就是使用自增语法incr
实现,然后再使用查询语句将对应的结果查询出来,但是有一个问题就是,我们需要保证原子性,这时候我们就要想到Lua
脚本了;另外为了提升获取序列号的性能,我们最好应用池化的思想创建一个序列号池:
Lua脚本
:
--[不同的业务不同的key]
local key=KEYS[1]
--[初始值]
local initValue=tonumber(ARGV[1])
--[最大值]
local maxValue=tonumber(ARGV[2])
--[池化最小数量]
local minCount=tonumber(ARGV[3])
--[池化初始化数量]
local initCount=tonumber(ARGV[4])
--[池化扩容步长]
local expansionStep=tonumber(ARGV[5])
--[自增步长]
local incrStep=tonumber(ARGV[6])
--[获取队列长度]
local len=redis.call('llen',key)
--[是否需要初始化,0:第一次调用只初始化,1:第一次调用初始化并返回序列号]
local isInit=tonumber(ARGV[7])<=0
--[循环次数默认为池化最小数量]
local loop=initCount
--[初始化nextValue]
local nextValue=initValue
--[如果队列长度大于最小数量,直接返回队列最左值]
if len>minCount
then
return redis.call('lpop',key)
end
--[如果队列长度小于最小数量,那么要准备补充序列号]
if len>0
then
--[计算需要循环的次数,当前长度+扩容步长]
loop=len+expansionStep
nextValue=tonumber(redis.call('rpop',key))
end
--[循环创建,直至最大值后重置为初始值]
while(len<loop)
do
if nextValue>maxValue
then
nextValue=initValue
end
redis.call('rpush',key,nextValue)
nextValue=nextValue+incrStep
len=len+1
end
--[如果只进行初始化,那么返回success,不返回序列号]
if isInit
then
return 'success'
end
--[返回序列号]
return redis.call('lpop',key)
1.在redis中生成一个队列,指定初始化长度,第一个初始值,最大值,队列最小数量,每次扩容的数量,自增的步长;
2.如果队列不存在,就初始化队列,按照给定的初始化长度,初始值,自增步长,最大值等参数创建一个队列;
3.如果队列中值的数量超过队列最小数量,那么直接pop出一个值;
4.如果小于最小数量,那么直接循环生成指定步长的自增ID;
5.如果表明此次是初始化的话,会返回success,否则就直接pop出第一个ID;
- java代码调用Lua脚本
private static final String LUA_SCRIPT = "LUA脚本";
private String executeLua(
String key,
int initValue,
int maxValue,
int minCount,
int initCount,
int expansionStep,
int incrStep,
int isInit) {
// 执行lua脚本
DefaultRedisScript<String> defaultRedisScript = new DefaultRedisScript<>();
defaultRedisScript.setResultType(String.class);
defaultRedisScript.setScriptText(LUA_SCRIPT);
RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
String result =
CastUtil.castString(
redisTemplate.execute(
defaultRedisScript,
serializer,
serializer,
Lists.newArrayList(key),
CastUtil.castString(initValue),
CastUtil.castString(maxValue),
CastUtil.castString(minCount),
CastUtil.castString(initCount),
CastUtil.castString(expansionStep),
CastUtil.castString(incrStep),
CastUtil.castString(isInit)));
return result;
}
1.我们在java中通过RedisTemplate来执行
Lua
脚本;2.该方法提供了两个功能,一个是初始化序列号池,另一个是初始化并返回序列号;当我们传递的
isInit=0
时仅仅会初始化序列号池,并返回success
告知初始化成功;isInit=1
时,会真正地返回我们需要的序列号;