背景
項目中使用mysql作為數(shù)據(jù)庫页畦,針對項目中各種需要自增返回序列碼值的場景(批次ID數(shù)據(jù)、自定義規(guī)則的序列碼ID等)需要提供一個序列碼池表進行維護煌妈。
實際使用中我們使用mysql的存儲過程進行實現(xiàn)夯接,同時示例如何使用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 '重置序列值,規(guī)則(none:不重置,month:按月重置,day:按天重置)',
PRIMARY KEY (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通用序列碼池表';
字段特殊說明
step_value
:每次自增的步長值,一般為1
reset_rule
:有些需求要求按照月或日重置value值為初始值,例如:每個月或每天從001開始,則設置該字段
示例
image.png
存儲過程(next_seq_value)
-- -------------------------------------------------------------------
-- seq_key 入?yún)? 對應序列碼入?yún)嶓w中的seqKey 查詢key
-- seq_value 出參 對應序列碼入?yún)嶓w中的seqValue 返回value
-- -------------------------------------------------------------------
CREATE PROCEDURE `next_seq_value`(IN seq_key varchar(255), OUT seq_value bigint(20))
BEGIN
-- 定義序列值
DECLARE temp_value BIGINT default 0;
-- 獲取當前值、當前步長亦镶、重置規(guī)則日月、更新時間變量
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;
封裝存儲過程中入?yún)⑴c出參的Java實體類
import lombok.Data;
/**
* 獲取序列參數(shù)對象
*
* @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 入?yún)?[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 參數(shù)是一個封裝入?yún)⑴c出參的類 -->
<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>
* 規(guī)則:[pc+5位數(shù)字(00001開始,順序遞增)]
*
* @return 卡批次ID
*/
public String generatePcId() {
// 創(chuàng)建封裝入?yún)⒊鰠嶓w類實例
CommonSequenceParams params = new CommonSequenceParams();
// 設置查詢key值
params.setSeqKey("pc");
// 執(zhí)行存儲過程key對應value值+1
commonSequenceDao.nextSeqValue(params);
// 執(zhí)行獲取返回的序列碼值
return "pc" + String.format("%05d", params.getSeqValue());
}
并發(fā)控制
直接調用存儲過程獲取序列號的過程是不安全的爱咬,如果存在多線程情況下會導致序列碼重復
解決辦法:使用時加鎖進行控制
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;
//每個線程的執(zhí)行次數(shù)
private final int size;
//記錄多線程的總執(zhí)行次數(shù),保證高并發(fā)下的原子性
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與對應的執(zhí)行次數(shù):" + Thread.currentThread().getId() + "--->" + count);
}
}
}
3.測試
/**
* 序列池測試
*
* @author sdevil507
*/
@SpringBootTest
public class CommonSequenceDaoTest {
@Autowired
private CommonSequenceDao commonSequenceDao;
/**
* 測試并發(fā)環(huán)境下序列碼生成性能
*/
@Test
public void testConcurrentSequencePerformance() throws InterruptedException {
//開啟的線程數(shù)
int threadSize = 150;
//創(chuàng)建線程池
ExecutorService executorService = Executors.newFixedThreadPool(threadSize);
//開始時間
long start = System.currentTimeMillis();
//讓線程池中的每一個線程都開始工作,最終生成目標150*150=22500序列碼數(shù)
for (int i = 0; i < threadSize; i++) {
//執(zhí)行線程
executorService.execute(new SequencePerformanceRunner(commonSequenceDao, threadSize));
}
//等線程全部執(zhí)行完后關閉線程池
executorService.shutdown();
//noinspection ResultOfMethodCallIgnored
executorService.awaitTermination(Integer.MAX_VALUE, TimeUnit.DAYS);
//結束時間
long end = System.currentTimeMillis();
System.out.println("測試次數(shù):" + SequencePerformanceRunner.atomicInteger.get());
System.out.println("用時(毫秒):" + (end - start));
System.out.println("速度(次/秒):" + SequencePerformanceRunner.atomicInteger.get() * 1000L / (end - start));
}
}
性能測試結果
上面性能測試代碼開啟150線程,每個線程執(zhí)行150次,總共生成150*150=22500個序列碼
結果如下:
測試次數(shù):22500
用時(毫秒):97346
速度(次/秒):231
每秒生成230個左右序列碼,目前對于我們的業(yè)務系統(tǒng)來說綽綽有余绊起!