目录
一、功能介绍
顺序号是项目开发中使用频率很高的基础服务之一,为每一个不同的业务数据生成一个唯一
的顺序号,一般为固定编码加日期,然后是顺序号。
比如 AAAA-20230721-00001 BBBB-20230721-00001。每次生成一条相同前缀的业务数据,
都会将顺序号递增,如果中间的日期变化了,那么顺序号会更新为新的日期加新开始的顺序号。
二、并发安全以及性能优化
当有多个请求同时访问顺序号服务会导致线程安全问题,比如多个线程同时保存顺序号时会抛出
主键重复异常,或者是会出现一些重复的顺序号,这些问题都是因为线程不安全造成的。本文会介绍通
过加锁来解决线程安全的问题,以及实现一把自定义的前缀编码锁,来提高顺序号的性能,使它能够在
生成相同前缀编码的顺序号时候保证顺序生成,而在生成不同前缀编码时可以并发执行。
三、并发技术以及数据库表设计
使用到的并发技术
ConcurrentHashMap 线程安全的Map
ThreadLocal 每个线程保存一份它自己的副本
Semaphore 信号量
CountDownLatch 闭锁
数据库表设计
表名 prefix_sequence
prefix_name varchar(255) 主键 前缀编码名字
width int 主键 宽度
date_flag int 主键 是否带日期(0否,1是)
sequence_value varchar(255) 顺序号
sequence_date datetime 日期
四、思路和代码实现
基础的顺序号实现
基础实体类,dao,service代码
/**
* 前缀顺序号实体类
*/
@Data
public class PrefixSequence {
// 前缀名字
private String prefixName;
// 宽度
private Integer width;
// 是否带日期(0否,1是)
private Integer dateFlag;
// 顺序号
private String sequenceValue;
// 日期
private Date sequenceDate;
}
/**
* 顺序号mapper
*/
public interface PrefixSequenceMapper {
int deleteByPrimaryKey(@Param("prefixName") String prefixName, @Param("width") Integer width, @Param("dateFlag") Integer dateFlag);
int insert(PrefixSequence record);
int insertSelective(PrefixSequence record);
PrefixSequence selectByPrimaryKey(@Param("prefixName") String prefixName, @Param("width") Integer width, @Param("dateFlag") Integer dateFlag);
int updateByPrimaryKeySelective(PrefixSequence record);
int updateByPrimaryKey(PrefixSequence record);
}
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="cc.myspring.sequence.mapper.PrefixSequenceMapper">
<resultMap id="BaseResultMap" type="cc.myspring.sequence.domain.PrefixSequence">
<id column="prefix_name" jdbcType="VARCHAR" property="prefixName" />
<id column="width" jdbcType="INTEGER" property="width" />
<id column="date_flag" jdbcType="INTEGER" property="dateFlag" />
<result column="sequence_value" jdbcType="VARCHAR" property="sequenceValue" />
<result column="sequence_date" jdbcType="TIMESTAMP" property="sequenceDate" />
</resultMap>
<sql id="Base_Column_List">
prefix_name, width, date_flag, sequence_value, sequence_date
</sql>
<select id="selectByPrimaryKey" parameterType="map" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from prefix_sequence
where prefix_name = #{prefixName,jdbcType=VARCHAR}
and width = #{width,jdbcType=INTEGER}
and date_flag = #{dateFlag,jdbcType=INTEGER}
</select>
<delete id="deleteByPrimaryKey" parameterType="map">
delete from prefix_sequence
where prefix_name = #{prefixName,jdbcType=VARCHAR}
and width = #{width,jdbcType=INTEGER}
and date_flag = #{dateFlag,jdbcType=INTEGER}
</delete>
<insert id="insert" parameterType="cc.myspring.sequence.domain.PrefixSequence">
insert into prefix_sequence (prefix_name, width, date_flag,
sequence_value, sequence_date)
values (#{prefixName,jdbcType=VARCHAR}, #{width,jdbcType=INTEGER}, #{dateFlag,jdbcType=INTEGER},
#{sequenceValue,jdbcType=VARCHAR}, #{sequenceDate,jdbcType=TIMESTAMP})
</insert>
<insert id="insertSelective" parameterType="cc.myspring.sequence.domain.PrefixSequence">
insert into prefix_sequence
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="prefixName != null">
prefix_name,
</if>
<if test="width != null">
width,
</if>
<if test="dateFlag != null">
date_flag,
</if>
<if test="sequenceValue != null">
sequence_value,
</if>
<if test="sequenceDate != null">
sequence_date,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="prefixName != null">
#{prefixName,jdbcType=VARCHAR},
</if>
<if test="width != null">
#{width,jdbcType=INTEGER},
</if>
<if test="dateFlag != null">
#{dateFlag,jdbcType=INTEGER},
</if>
<if test="sequenceValue != null">
#{sequenceValue,jdbcType=VARCHAR},
</if>
<if test="sequenceDate != null">
#{sequenceDate,jdbcType=TIMESTAMP},
</if>
</trim>
</insert>
<update id="updateByPrimaryKeySelective" parameterType="cc.myspring.sequence.domain.PrefixSequence">
update prefix_sequence
<set>
<if test="sequenceValue != null">
sequence_value = #{sequenceValue,jdbcType=VARCHAR},
</if>
<if test="sequenceDate != null">
sequence_date = #{sequenceDate,jdbcType=TIMESTAMP},
</if>
</set>
where prefix_name = #{prefixName,jdbcType=VARCHAR}
and width = #{width,jdbcType=INTEGER}
and date_flag = #{dateFlag,jdbcType=INTEGER}
</update>
<update id="updateByPrimaryKey" parameterType="cc.myspring.sequence.domain.PrefixSequence">
update prefix_sequence
set sequence_value = #{sequenceValue,jdbcType=VARCHAR},
sequence_date = #{sequenceDate,jdbcType=TIMESTAMP}
where prefix_name = #{prefixName,jdbcType=VARCHAR}
and width = #{width,jdbcType=INTEGER}
and date_flag = #{dateFlag,jdbcType=INTEGER}
</update>
</mapper>
/**
* 前缀顺序号服务
*/
public interface PrefixSequenceService {
/**
* 生成带日期的顺序号
* @param prefixName
* @param width
* @return
*/
String generateSequenceDateValue(String prefixName, Integer width) throws Exception;
/**
* 1 生成带日期的线程安全的顺序号
* @param prefixName
* @param width
* @return
* @throws Exception
*/
String generateSynSequenceDateValue(String prefixName, Integer width) throws Exception;
/**
* 2 生成带日期的线程安全的顺序号 相同的前缀名加锁,不同的前缀名可以并行
* @param prefixName
* @param width
* @return
* @throws Exception
*/
String generateSynKeyLockSequenceDateValue(String prefixName, Integer width) throws Exception;
}
/**
* 前缀顺序号服务实现
*/
@Service
public class PrefixSequenceServiceImpl implements PrefixSequenceService {
@Autowired
PrefixSequenceMapper prefixSequenceMapper;
@Override
public String generateSequenceDateValue(String prefixName, Integer width) throws Exception{
if(StringUtils.isEmpty(prefixName)||null == width){
throw new Exception("参数不能为空");
}
// 查询数据库顺序号
PrefixSequence prefixSequence = prefixSequenceMapper.selectByPrimaryKey(prefixName,width,1);
// 如果没有就新建
if(prefixSequence==null){
String newSequence = generateNewSequence(prefixName,width,1);
SimpleDateFormat dateFormat=new SimpleDateFormat("yyyyMMdd");
String format = dateFormat.format(new Date());
return prefixName+"-"+format+"-"+newSequence;
}else{
SimpleDateFormat dateFormat=new SimpleDateFormat("yyyyMMdd");
// 如果是当天,递增顺序号更新并返回
if(SequenceStringUtil.judgeSameDay(prefixSequence)){
// 将顺序号转换为Long
Long value = Long.valueOf(prefixSequence.getSequenceValue());
String updateValue = SequenceStringUtil.formatSequenceValue(width,++value);
prefixSequence.setSequenceValue(updateValue);
prefixSequenceMapper.updateByPrimaryKeySelective(prefixSequence);
// 返回更新后的顺序号
String updateSequence = prefixSequence.getPrefixName()+"-"+
dateFormat.format(prefixSequence.getSequenceDate())+"-"+
prefixSequence.getSequenceValue();
return updateSequence;
}else{
// 如果不是当天,更新顺序号(更新当前日期,如果宽度为5,顺序号为00001)并返回。
String newSequence = SequenceStringUtil.formatSequenceValue(width, 1L);
prefixSequence.setSequenceValue(newSequence);
prefixSequence.setSequenceDate(new Date());
prefixSequenceMapper.updateByPrimaryKeySelective(prefixSequence);
// 返回更新后的顺序号
String updateSequence = prefixSequence.getPrefixName()+"-"+
dateFormat.format(prefixSequence.getSequenceDate())+"-"+
prefixSequence.getSequenceValue();
return updateSequence;
}
}
}
/**
* 辅助方法
* 创建一个新的顺序号,指定宽度,从1开始
* @param prefixName
* @param width
* @param dateFlag
* @return
*/
private String generateNewSequence(String prefixName, Integer width, Integer dateFlag) {
// 创建一个新的顺序号,指定宽度,从1开始
String newSequence = SequenceStringUtil.formatSequenceValue(width, 1L);
// 保存顺序号
PrefixSequence prefixSequence = new PrefixSequence();
prefixSequence.setPrefixName(prefixName);
prefixSequence.setWidth(width);
prefixSequence.setSequenceValue(newSequence);
prefixSequence.setSequenceDate(new Date());
prefixSequence.setDateFlag(dateFlag);
prefixSequenceMapper.insertSelective(prefixSequence);
// 返回新生成的顺序号
return newSequence;
}
}
/**
* 顺序号工具类
* 使用 StringBuilder 以及 DecimalFormat 填充前缀0
*/
public class SequenceStringUtil {
// 填充0并格式化值
public static String formatSequenceValue(Integer sequenceWidth, Long value){
StringBuilder sb = new StringBuilder();
for(int i=0;i<sequenceWidth;i++){
sb.append("0");
}
DecimalFormat decimalFormat = new DecimalFormat(sb.toString());
String resultValue = decimalFormat.format(value);
return resultValue;
}
//判断是否为同一天
public static boolean judgeSameDay(PrefixSequence sequence) {
SimpleDateFormat dateFormat=new SimpleDateFormat("yyyyMMdd");
return dateFormat.format(sequence.getSequenceDate()).equals(dateFormat.format(new Date()));
}
}
生成顺序号核心逻辑
a) 根据前缀名,宽度,是否带日期查询数据库中的顺序号。
b) 如果没有找到,生成新的顺序号(比如宽度为5,从00001开始)保存并返回。
c) 如果已经有该前缀以及宽度的顺序号,如果日期相同,将顺序号递增,更新并返回。
d) 如果已经有该前缀以及宽度的顺序号,如果日期不相同,更新日期(当天)以及初始化顺序号
(比如宽度为5,从00001开始)并返回。
基础的顺序号服务就开发完了,我们来测试一下。
@RestController
@RequestMapping("/sequence")
public class PrefixSequenceController {
@Autowired
PrefixSequenceService prefixSequenceService;
/**
* 生成带日期的顺序号
* @param prefixName
* @param width
* @return
* @throws Exception
*/
@GetMapping("/generateSequenceDateValue/{prefixName}/{width}")
public AjaxResult generateSequenceDateValue(@PathVariable("prefixName") String prefixName, @PathVariable("width") Integer width) throws Exception {
return AjaxResult.success("成功",prefixSequenceService.generateSequenceDateValue(prefixName,width));
}
}
单次请求返回结果大致如下
AAAA-20230721-00001
AAAA-20230721-00002
AAAA-20230721-00003
BBBB-20230721-00001
BBBB-20230721-00002
返回了预期的结果。
下面我们模拟高并发场景下的顺序号生成
@RestController
@RequestMapping("/sequence")
public class PrefixSequenceController {
@Autowired
PrefixSequenceService prefixSequenceService;
/**
* 高并发下测试顺序号
* @param prefixName
* @param width
* @return
* @throws Exception
*/
@GetMapping("/test/generateSequenceDateValue/{prefixName}/{width}")
public AjaxResult testGenerateSequenceDateValue(@PathVariable("prefixName") String prefixName, @PathVariable("width") Integer width) throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(100);
class PrefixSequenceRun implements Runnable {
@SneakyThrows
@Override
public void run() {
countDownLatch.countDown();
countDownLatch.await();
String sequence = prefixSequenceService.generateSequenceDateValue(prefixName,width);
System.out.println(sequence);
}
}
for(int i=0;i<100;i++){
new Thread(new PrefixSequenceRun()).start();
}
return AjaxResult.success();
}
}
使用闭锁模拟100次同时请求顺序号服务(前缀为AAAA,宽度为5)。
输出大致结果如下
ERROR:Duplicate entry 'AAAA-5-1' for key 'prefix_sequence.PRIMARY'
AAAA-20230721-00002
AAAA-20230721-00002
AAAA-20230721-00002
AAAA-20230721-00002
AAAA-20230721-00002
AAAA-20230721-00003
AAAA-20230721-00003
AAAA-20230721-00003
我们发现并没有按照我们预期的产生正确的顺序号,有抛出主键重复的异常,并且生成了重复的顺序号。
这是为什么呢?这是因为我们没有考虑到线程安全的问题,在基础的顺序号生成服务中,如果有
2个线程同时访问了前缀为AAAA宽度为5的服务,第一个线程发现数据库中没有找到信息,所以就生
成一个新的顺序号,在第一个线程还没有保存的时候,第二个线程进来了,它也发现没有找到前缀为
AAAA宽度为5的数据所以也生成了一个新的顺序号,当第一个线程保存了顺序号以后,第二个线程
才执行保存操作,所以就抛出了主键重复的异常。重复的顺序号也是因为线程不安全造成的。
那么我们怎么解决线程安全的问题呢?答案是在服务入口处加一把锁。
/**
* 前缀顺序号服务实现
*/
@Service
public class PrefixSequenceServiceImpl implements PrefixSequenceService {
@Autowired
PrefixSequenceMapper prefixSequenceMapper;
// 可重入锁
Lock lock = new ReentrantLock();
@Override
public String generateSynSequenceDateValue(String prefixName, Integer width) throws Exception {
if(StringUtils.isEmpty(prefixName)||null == width){
throw new Exception("参数不能为空");
}
// 在逻辑入口处加锁,粒度粗,服务吞吐低
lock.lock();
try{
// 查询数据库顺序号
PrefixSequence prefixSequence = prefixSequenceMapper.selectByPrimaryKey(prefixName,width,1);
// 如果没有就新建
if(prefixSequence==null){
String newSequence = generateNewSequence(prefixName,width,1);
SimpleDateFormat dateFormat=new SimpleDateFormat("yyyyMMdd");
String format = dateFormat.format(new Date());
return prefixName+"-"+format+"-"+newSequence;
}else{
SimpleDateFormat dateFormat=new SimpleDateFormat("yyyyMMdd");
// 如果是当天
if(SequenceStringUtil.judgeSameDay(prefixSequence)){
// 将顺序号转换为Long
Long value = Long.valueOf(prefixSequence.getSequenceValue());
String updateValue = SequenceStringUtil.formatSequenceValue(width,++value);
prefixSequence.setSequenceValue(updateValue);
prefixSequenceMapper.updateByPrimaryKeySelective(prefixSequence);
// 返回更新后的顺序号
String updateSequence = prefixSequence.getPrefixName()+"-"+
dateFormat.format(prefixSequence.getSequenceDate())+"-"+
prefixSequence.getSequenceValue();
return updateSequence;
}else{
String newSequence = SequenceStringUtil.formatSequenceValue(width, 1L);
prefixSequence.setSequenceValue(newSequence);
prefixSequence.setSequenceDate(new Date());
prefixSequenceMapper.updateByPrimaryKeySelective(prefixSequence);
// 返回更新后的顺序号
String updateSequence = prefixSequence.getPrefixName()+"-"+
dateFormat.format(prefixSequence.getSequenceDate())+"-"+
prefixSequence.getSequenceValue();
return updateSequence;
}
}
}finally {
lock.unlock();
}
}
/**
* 辅助方法
* 创建一个新的顺序号,指定宽度,从1开始
* @param prefixName
* @param width
* @param dateFlag
* @return
*/
private String generateNewSequence(String prefixName, Integer width, Integer dateFlag) {
// 创建一个新的顺序号,指定宽度,从1开始
String newSequence = SequenceStringUtil.formatSequenceValue(width, 1L);
// 保存顺序号
PrefixSequence prefixSequence = new PrefixSequence();
prefixSequence.setPrefixName(prefixName);
prefixSequence.setWidth(width);
prefixSequence.setSequenceValue(newSequence);
prefixSequence.setSequenceDate(new Date());
prefixSequence.setDateFlag(dateFlag);
prefixSequenceMapper.insertSelective(prefixSequence);
// 返回新生成的顺序号
return newSequence;
}
}
我们使用 ReentrantLock 来为服务加一把锁。
再次测试
这次输出的结果正确了,我们解决了线程安全的问题。
但是如果我们同时请求不同前缀的顺序号会是什么样的情况呢?
测试代码
@RestController
@RequestMapping("/sequence")
public class PrefixSequenceController {
@Autowired
PrefixSequenceService prefixSequenceService;
/**
* 高并发下测试顺序号,1 在逻辑入口处加锁,粒度粗,服务吞吐低
* @param prefixName
* @param width
* @return
* @throws Exception
*/
@GetMapping("/test2/generateSequenceDateValue/{prefixName}/{width}")
public AjaxResult test2GenerateSequenceDateValue(@PathVariable("prefixName") String prefixName, @PathVariable("width") Integer width) throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(100);
class PrefixSequenceRunA implements Runnable {
@SneakyThrows
@Override
public void run() {
countDownLatch.countDown();
countDownLatch.await();
String sequence = prefixSequenceService.generateSynSequenceDateValue("AAAA",width);
System.out.println(sequence);
}
}
class PrefixSequenceRunB implements Runnable {
@SneakyThrows
@Override
public void run() {
countDownLatch.countDown();
countDownLatch.await();
String sequence = prefixSequenceService.generateSynSequenceDateValue("BBBB",width);
System.out.println(sequence);
}
}
for(int i=0;i<50;i++){
new Thread(new PrefixSequenceRunA()).start();
}
for(int i=0;i<50;i++){
new Thread(new PrefixSequenceRunB()).start();
}
return AjaxResult.success();
}
}
这次我们分别请求50次前缀为AAAA宽度为5和前缀为BBBB宽度为5的顺序号
输出大致结果如下
AAAA-20230721-00001
BBBB-20230721-00001
AAAA-20230721-00002
BBBB-20230721-00002
AAAA-20230721-00003
AAAA-20230721-00004
BBBB-20230721-00003
AAAA-20230721-00005
…
BBBB-20230721-00004
BBBB-20230721-00005
…
如果在顺序号服务中让线程休眠1秒,我们发现在同一时间,也就是1秒内只能生成一个顺
序号。不管是相同的前缀编码还是不同的前缀编码,在同一时刻只能生成一个顺序号,这种吞吐量
在真实的项目环境中是很差的,我们还需要继续优化。
为了保证线程的安全,相同前缀的的顺序号必须要保证串行执行。但是不同前缀的顺序号是
可以并行执行的,这样就大大优化了并发性能。
思想仍然是加锁,但是同一前缀的顺序号生成时加锁,不同前缀的顺序号在同一时刻可以并
行,(其实也加锁了,只不过每个不同前缀的唯一一个访问线程在并发执行)
我们需要自定义一把显示锁(JDK提供的锁或并发工具没有直接实现该功能的),定义类似于
keyLock.lock(prefixName); 的加锁方法,以及 keyLock.unlock(prefixName); 的解锁方法,来替换服
务中的 ReentrantLock。
自定义锁的实现代码
/**
* 前缀锁
* @param <K>
*/
public class PrefixSequenceKeyLock<K> {
// 用于保存锁定的key和信号量
private final ConcurrentMap<K, Semaphore> semaphoreMap = new ConcurrentHashMap<K, Semaphore>();
// 用于保存每个线程锁定的key
private final ThreadLocal<Map<K, LockInnerSupport>> threadLocal = new ThreadLocal<Map<K, LockInnerSupport>>() {
@Override
protected Map<K, LockInnerSupport> initialValue() {
return new HashMap<K, LockInnerSupport>();
}
};
/**
* 对同一Key加锁,不同Key可以并发执行。
* @param key
*/
public void lock(K key) {
if (key == null){
return;
}
// 获取本线程绑定key的map(local)
LockInnerSupport lockSupport = threadLocal.get().get(key);
// 如果是本线程第一次访问,那么info为空
if (lockSupport == null) {
// 创建1个许可证的信号量
Semaphore current = new Semaphore(1);
// 获取许可
current.acquireUninterruptibly();
// 返回上一个线程保存的该key的信号量
// 如果key不存在,put值并返回null。如果key存在,put更新值并返回旧值。
Semaphore previous = semaphoreMap.put(key, current);
// 当第二个以后的线程访问该key previous 获取的是上一个线程保存的信号量
// 当key不存在时、即第一次semaphoreMap保存key,previous为空
if (previous != null){
// 相同key的顺序号生成线程会阻塞在这里直到上一个线程释放锁(许可证)
// 如果previous不为空,那么它的许可证为0,所以再次获取许可证时会被阻塞在这里,直到上一个访问该key的线程释放许可证才会继续执行
previous.acquireUninterruptibly();
}
// 更新local把该key对应的LockInfo放入ThreadLocal
threadLocal.get().put(key, new LockInnerSupport(current));
} else {
// 支持可重入
lockSupport.lockCount++;
}
}
/**
* 释放key,唤醒其他等待该key的线程
* @param key
*/
public void unlock(K key) {
if (key == null){
return;
}
// 获取当前线程对应key的信号量
LockInnerSupport lockSupport = threadLocal.get().get(key);
// 释放许可证,支持可重入
if (lockSupport != null && --lockSupport.lockCount == 0) {
// 释放许可,唤醒该key中阻塞的其他线程
lockSupport.current.release();
// 删除该key
semaphoreMap.remove(key, lockSupport.current);
// 删除该key,防止OOM
threadLocal.get().remove(key);
}
}
/**
* 封装信号量和计数的内部服务支持类
*/
private static class LockInnerSupport {
private final Semaphore current;
private int lockCount;
private LockInnerSupport(Semaphore current) {
this.current = current;
this.lockCount = 1;
}
}
}
使用ConcurrentMap来保存锁定的key和信号量。
使用ThreadLocal来保存每一个线程自己的锁定key。
用Semaphore来控制线程的阻塞和唤醒。
关键在于这里
Semaphore previous = semaphoreMap.put(key, current);
if (previous != null){
previous.acquireUninterruptibly();
}
还是拿前缀编码AAAA 和 宽度5 来举例。
当已经有一个线程在生成AAAA宽度为5的顺序号时,它会把自己的Key和许可证已经减1的信号量注
册到semaphoreMap中, 在这个线程还没有释放锁的时候,下一个访问Key为AAAA宽度为5的顺序号线程
也把自己的Key和许可证已经减1的信号量注册到semaphoreMap中,并且得到上一个线程保存在semaphoreMap
的信号量,然后再次扣减1个许可证,变为了-1,这样就可以阻塞住第二个访问的线程,直到第一个线程释放锁
才会唤醒第二个线程继续执行。
替换为自定义显示锁的业务代码
/**
* 前缀顺序号服务实现
*/
@Service
public class PrefixSequenceServiceImpl implements PrefixSequenceService {
@Autowired
PrefixSequenceMapper prefixSequenceMapper;
// 自定义锁,相同前缀加锁,不同前缀并发执行
private PrefixSequenceKeyLock<String> keyLock = new PrefixSequenceKeyLock<String>();
@Override
public String generateSynKeyLockSequenceDateValue(String prefixName, Integer width) throws Exception {
if(StringUtils.isEmpty(prefixName)||null == width){
throw new Exception("参数不能为空");
}
// 相同的前缀名加锁,不同的前缀名可以并行,力度细,服务吞吐高
keyLock.lock(prefixName);
try{
// 查询数据库顺序号
PrefixSequence prefixSequence = prefixSequenceMapper.selectByPrimaryKey(prefixName,width,1);
// 如果没有就新建
if(prefixSequence==null){
String newSequence = generateNewSequence(prefixName,width,1);
SimpleDateFormat dateFormat=new SimpleDateFormat("yyyyMMdd");
String format = dateFormat.format(new Date());
return prefixName+"-"+format+"-"+newSequence;
}else{
SimpleDateFormat dateFormat=new SimpleDateFormat("yyyyMMdd");
// 如果是当天
if(SequenceStringUtil.judgeSameDay(prefixSequence)){
// 将顺序号转换为Long
Long value = Long.valueOf(prefixSequence.getSequenceValue());
String updateValue = SequenceStringUtil.formatSequenceValue(width,++value);
prefixSequence.setSequenceValue(updateValue);
prefixSequenceMapper.updateByPrimaryKeySelective(prefixSequence);
// 返回更新后的顺序号
String updateSequence = prefixSequence.getPrefixName()+"-"+
dateFormat.format(prefixSequence.getSequenceDate())+"-"+
prefixSequence.getSequenceValue();
return updateSequence;
}else{
String newSequence = SequenceStringUtil.formatSequenceValue(width, 1L);
prefixSequence.setSequenceValue(newSequence);
prefixSequence.setSequenceDate(new Date());
prefixSequenceMapper.updateByPrimaryKeySelective(prefixSequence);
// 返回更新后的顺序号
String updateSequence = prefixSequence.getPrefixName()+"-"+
dateFormat.format(prefixSequence.getSequenceDate())+"-"+
prefixSequence.getSequenceValue();
return updateSequence;
}
}
}finally {
keyLock.unlock(prefixName);
}
}
/**
* 辅助方法
* 创建一个新的顺序号,指定宽度,从1开始
* @param prefixName
* @param width
* @param dateFlag
* @return
*/
private String generateNewSequence(String prefixName, Integer width, Integer dateFlag) {
// 创建一个新的顺序号,指定宽度,从1开始
String newSequence = SequenceStringUtil.formatSequenceValue(width, 1L);
// 保存顺序号
PrefixSequence prefixSequence = new PrefixSequence();
prefixSequence.setPrefixName(prefixName);
prefixSequence.setWidth(width);
prefixSequence.setSequenceValue(newSequence);
prefixSequence.setSequenceDate(new Date());
prefixSequence.setDateFlag(dateFlag);
prefixSequenceMapper.insertSelective(prefixSequence);
// 返回新生成的顺序号
return newSequence;
}
}
我们再次测试不同前缀的服务请求。
测试代码
@RestController
@RequestMapping("/sequence")
public class PrefixSequenceController {
@Autowired
PrefixSequenceService prefixSequenceService;
/**
* 高并发下测试顺序号,2 相同的前缀名加锁,不同的前缀名可以并行,粒度细,服务吞吐高
* @param prefixName
* @param width
* @return
* @throws Exception
*/
@GetMapping("/test3/generateSequenceDateValue/{prefixName}/{width}")
public AjaxResult test3GenerateSequenceDateValue(@PathVariable("prefixName") String prefixName, @PathVariable("width") Integer width) throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(150);
class PrefixSequenceRunA implements Runnable {
@SneakyThrows
@Override
public void run() {
countDownLatch.countDown();
countDownLatch.await();
String sequence = prefixSequenceService.generateSynKeyLockSequenceDateValue("AAAA",width);
System.out.println(sequence);
}
}
class PrefixSequenceRunB implements Runnable {
@SneakyThrows
@Override
public void run() {
countDownLatch.countDown();
countDownLatch.await();
String sequence = prefixSequenceService.generateSynKeyLockSequenceDateValue("BBBB",width);
System.out.println(sequence);
}
}
class PrefixSequenceRunC implements Runnable {
@SneakyThrows
@Override
public void run() {
countDownLatch.countDown();
countDownLatch.await();
String sequence = prefixSequenceService.generateSynKeyLockSequenceDateValue("CCCC",width);
System.out.println(sequence);
}
}
for(int i=0;i<50;i++){
new Thread(new PrefixSequenceRunA()).start();
}
for(int i=0;i<50;i++){
new Thread(new PrefixSequenceRunB()).start();
}
for(int i=0;i<50;i++){
new Thread(new PrefixSequenceRunC()).start();
}
return AjaxResult.success();
}
}
这次我们分别请求50次前缀为AAAA宽度为5和前缀为BBBB宽度为5和前缀为CCCC宽度为5的顺序号。
输出大致结果如下
AAAA-20230721-00001
BBBB-20230721-00001
CCCC-20230721-00001
AAAA-20230721-00002
BBBB-20230721-00002
CCCC-20230721-00002
AAAA-20230721-00003
BBBB-20230721-00003
CCCC-20230721-00003
AAAA-20230721-00004
BBBB-20230721-00004
CCCC-20230721-00004
…
可以看到,同一时刻,三种不同的前缀顺序号是并发生成的。
五、源代码下载
源码下载地址 gitcode
https://gitcode.net/muzili90K/openthread