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}
```
/**
* 前缀顺序号服务
*/
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
# 分享
**1、算法大厂——字节跳动面试题**
![](https://img-blog.csdnimg.cn/img_convert/35ee54da74e0f00d1d27f2d6e3499b77.webp?x-oss-process=image/format,png)
**2、2000页互联网Java面试题大全**
![](https://img-blog.csdnimg.cn/img_convert/83d89313ce8354a68e976e7c613b0d4e.webp?x-oss-process=image/format,png)
**3、高阶必备,算法学习**
![](https://img-blog.csdnimg.cn/img_convert/be3d8f2421f3b99ab1a8034edcf4e002.webp?x-oss-process=image/format,png)
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
# 分享
**1、算法大厂——字节跳动面试题**
[外链图片转存中...(img-Ftk6CXxS-1719270793886)]
**2、2000页互联网Java面试题大全**
[外链图片转存中...(img-gZj9PrjD-1719270793886)]
**3、高阶必备,算法学习**
[外链图片转存中...(img-f1KmsSPu-1719270793887)]