背景
项目中使用mysql作为数据库,针对项目中各种需要自增返回序列码值的场景(批次ID数据、自定义规则的序列码ID等)需要提供一个序列码池表进行维护。
实际使用中我们使用mysql的存储过程进行实现,同时示例如何使用mybatis进行调用。
通用序列码池表(common_sequence)
-- ----------------------------
-- Table structure for common_sequence
-- ----------------------------
DROP TABLE IF EXISTS `common_sequence`;
CREATE TABLE `common_sequence` (
`key` varchar(40) NOT NULL COMMENT '序列key',
`value` bigint NOT NULL DEFAULT '0' COMMENT '序列value',
`remark` varchar(255) CHARACTER SET utf8mb4 NOT NULL COMMENT '备注说明',
`step_value` int NOT NULL DEFAULT '1' COMMENT '步长值(每次累加值)',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`reset_rule` enum('none','month','day') CHARACTER SET utf8mb4 NOT NULL DEFAULT 'none' COMMENT '重置序列值,规则(none:不重置,month:按月重置,day:按天重置)',
PRIMARY KEY (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通用序列码池表';
字段特殊说明
step_value
:每次自增的步长值,一般为1
reset_rule
:有些需求要求按照月或日重置value值为初始值,例如:每个月或每天从001开始,则设置该字段
示例
存储过程(next_seq_value)
-- -------------------------------------------------------------------
-- seq_key 入参 对应序列码入参实体中的seqKey 查询key
-- seq_value 出参 对应序列码入参实体中的seqValue 返回value
-- -------------------------------------------------------------------
CREATE PROCEDURE `next_seq_value`(IN seq_key varchar(255), OUT seq_value bigint(20))
BEGIN
-- 定义序列值
DECLARE temp_value BIGINT default 0;
-- 获取当前值、当前步长、重置规则、更新时间变量
SELECT value,step_value,reset_rule,update_time INTO @current_value,@current_step_value,@reset_rule,@update_time FROM COMMON_SEQUENCE t WHERE t.key =seq_key;
CASE
WHEN @reset_rule ='month' THEN
-- 判断当前日期与更新日期是否同一个月
IF DATE_FORMAT(@update_time,'%Y%m')=DATE_FORMAT(now(),'%Y%m') THEN
-- 如果是同一个月,序列值:[当前序列值+当前步长]
SET temp_value=@current_value + @current_step_value;
ELSE
-- 如果不是同一个月,序列值:[序列值设置为0+当前步长]
SET temp_value=temp_value + @current_step_value;
END IF;
WHEN @reset_rule ='day' THEN
-- 判断当前日期与更新日期是否同一天
IF DATE_FORMAT(@update_time,'%Y%m%d')=DATE_FORMAT(now(),'%Y%m%d') THEN
-- 如果是同一个天,序列值:[当前序列值+当前步长]
SET temp_value=@current_value + @current_step_value;
ELSE
-- 如果不是同一个天,序列值:[序列值设置为0+当前步长]
SET temp_value=temp_value + @current_step_value;
END IF;
ELSE
-- 默认'none',序列值:[当前序列值+当前步长]
SET temp_value=@current_value + @current_step_value;
END CASE;
-- 更新序列表
UPDATE COMMON_SEQUENCE t1
SET t1.value = temp_value
WHERE t1.key = seq_key AND @current_value = t1.value;
-- 将序列值返回
SELECT temp_value INTO seq_value;
END;
封装存储过程中入参与出参的Java实体类
import lombok.Data;
/**
* 获取序列参数对象
*
* @author sdevil507
* created on 2021/4/6
*/
@Data
public class CommonSequenceParams {
/**
* [IN]序列码的key
*/
private String seqKey;
/**
* [OUT]序列码的值
*/
private long seqValue;
}
mybatis dao示例
import org.apache.ibatis.annotations.Mapper;
/**
* 序列码池Dao
*
* @author sdevil507
* created on 2021/4/6
*/
@Mapper
public interface CommonSequenceDao {
/**
* 获取下一序列值
*
* @param params 入参([IN:string]key=seqName,[OUT:long]key=seqNo)
*/
void nextSeqValue(CommonSequenceParams params);
}
mybatis 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.yama.eraims.equities.helper.seq.CommonSequenceDao">
<!--suppress SqlNoDataSourceInspection -->
<!--注意此处 parameterType 参数是一个封装入参与出参的类 -->
<select id="nextSeqValue" parameterType="com.xxx.xxx.CommonSequenceParams"
statementType="CALLABLE">
CALL next_seq_value(#{seqKey,mode=IN}, #{seqValue,mode=OUT,jdbcType=BIGINT})
</select>
</mapper>
方法中具体调用示例
/**
* 生成卡批次ID序列号
* <p>
* 规则:[pc+5位数字(00001开始,顺序递增)]
*
* @return 卡批次ID
*/
public String generatePcId() {
// 创建封装入参出参实体类实例
CommonSequenceParams params = new CommonSequenceParams();
// 设置查询key值
params.setSeqKey("pc");
// 执行存储过程key对应value值+1
commonSequenceDao.nextSeqValue(params);
// 执行获取返回的序列码值
return "pc" + String.format("%05d", params.getSeqValue());
}
并发控制
直接调用存储过程获取序列号的过程是不安全的,如果存在多线程情况下会导致序列码重复
解决办法:使用时加锁进行控制
1.提供全局共享对象锁
/**
* 序列码锁
*
* @author sdevil507
* created on 2023/4/24
*/
public interface SequenceLocker {
/**
* 全局共享对象锁
*/
Object lock = new Object();
}
2.调用处进行加锁控制(此处使用性能测试的代码)
/**
* 序列生成性能
*
* @author sdevil507
* created on 2023/4/24
*/
public class SequencePerformanceRunner implements Runnable {
// seq dao
private final CommonSequenceDao commonSequenceDao;
//每个线程的执行次数
private final int size;
//记录多线程的总执行次数,保证高并发下的原子性
public static AtomicInteger atomicInteger = new AtomicInteger(0);
public SequencePerformanceRunner(CommonSequenceDao commonSequenceDao, int size) {
this.commonSequenceDao = commonSequenceDao;
this.size = size;
}
@Override
public void run() {
int count = 0;
while (count < size) {
count++;
atomicInteger.getAndIncrement();
CommonSequenceParams params = new CommonSequenceParams();
params.setSeqKey("test_seq");
// 注意:此处加锁进行控制,生成序列码
synchronized (SequenceLocker.lock) {
commonSequenceDao.nextSeqValue(params);
}
System.out.println("线程ID与对应的执行次数:" + Thread.currentThread().getId() + "--->" + count);
}
}
}
3.测试
/**
* 序列池测试
*
* @author sdevil507
*/
@SpringBootTest
public class CommonSequenceDaoTest {
@Autowired
private CommonSequenceDao commonSequenceDao;
/**
* 测试并发环境下序列码生成性能
*/
@Test
public void testConcurrentSequencePerformance() throws InterruptedException {
//开启的线程数
int threadSize = 150;
//创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(threadSize);
//开始时间
long start = System.currentTimeMillis();
//让线程池中的每一个线程都开始工作,最终生成目标150*150=22500序列码数
for (int i = 0; i < threadSize; i++) {
//执行线程
executorService.execute(new SequencePerformanceRunner(commonSequenceDao, threadSize));
}
//等线程全部执行完后关闭线程池
executorService.shutdown();
//noinspection ResultOfMethodCallIgnored
executorService.awaitTermination(Integer.MAX_VALUE, TimeUnit.DAYS);
//结束时间
long end = System.currentTimeMillis();
System.out.println("测试次数:" + SequencePerformanceRunner.atomicInteger.get());
System.out.println("用时(毫秒):" + (end - start));
System.out.println("速度(次/秒):" + SequencePerformanceRunner.atomicInteger.get() * 1000L / (end - start));
}
}
性能测试结果
上面性能测试代码开启150线程,每个线程执行150次,总共生成150*150=22500个序列码
结果如下:
测试次数:22500
用时(毫秒):97346
速度(次/秒):231
每秒生成230个左右序列码,目前对于我们的业务系统来说绰绰有余!