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

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)]

  • 16
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值