Java并发编程实战、顺序号_java生成顺序增长序列

蜂信物联FastBee平台https://gitee.com/beecue/fastbee

阿里资料开源项目https://gitee.com/vip204888

百度低代码前端框架https://gitee.com/baidu/amis

OpenHarmony开源项目https://gitcode.com/openharmony

仓颉编程语言开放项目https://gitcode.com/Cangjie
@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
    */
    public class PrefixSequenceKeyLock {

    // 用于保存锁定的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 keyLock = new PrefixSequenceKeyLock();

    @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


        …


# 最后

关于面试刷题也是有方法可言的,建议最好是按照专题来进行,然后由基础到高级,由浅入深来,效果会更好。当然,这些内容我也全部整理在一份pdf文档内,分成了以下几大专题:

*   Java基础部分

![](https://img-blog.csdnimg.cn/img_convert/38cda247ef865176f1edecbf273512dd.webp?x-oss-process=image/format,png)

*   算法与编程

![](https://img-blog.csdnimg.cn/img_convert/317d31ba072c782ffce08b2241ea6e0d.webp?x-oss-process=image/format,png)

*   数据库部分

![](https://img-blog.csdnimg.cn/img_convert/781d1d46243fc5697265c4ff10d8d641.webp?x-oss-process=image/format,png)

*   流行的框架与新技术(Spring+SpringCloud+SpringCloudAlibaba)

![](https://img-blog.csdnimg.cn/img_convert/6e9c0b4bc037fed733bd256aa8172481.webp?x-oss-process=image/format,png)

这份面试文档当然不止这些内容,实际上像JVM、设计模式、ZK、MQ、数据结构等其他部分的面试内容均有涉及,因为文章篇幅,就不全部在这里阐述了。

**作为一名程序员,阶段性的学习是必不可少的,而且需要保持一定的持续性,这次在这个阶段内,我对一些重点的知识点进行了系统的复习,一方面巩固了自己的基础,另一方面也提升了自己的知识广度和深度。**
003


        BBBB-20230721-00003


        CCCC-20230721-00003


        AAAA-20230721-00004


        BBBB-20230721-00004


        CCCC-20230721-00004


        …


# 最后

关于面试刷题也是有方法可言的,建议最好是按照专题来进行,然后由基础到高级,由浅入深来,效果会更好。当然,这些内容我也全部整理在一份pdf文档内,分成了以下几大专题:

*   Java基础部分

[外链图片转存中...(img-2hyI4jJi-1725170164555)]

*   算法与编程

[外链图片转存中...(img-TlbphGP4-1725170164556)]

*   数据库部分

[外链图片转存中...(img-F9UU5ACf-1725170164556)]

*   流行的框架与新技术(Spring+SpringCloud+SpringCloudAlibaba)

[外链图片转存中...(img-d2GZK3Dw-1725170164557)]

这份面试文档当然不止这些内容,实际上像JVM、设计模式、ZK、MQ、数据结构等其他部分的面试内容均有涉及,因为文章篇幅,就不全部在这里阐述了。

**作为一名程序员,阶段性的学习是必不可少的,而且需要保持一定的持续性,这次在这个阶段内,我对一些重点的知识点进行了系统的复习,一方面巩固了自己的基础,另一方面也提升了自己的知识广度和深度。**
  • 24
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值