2024年最全Java并发编程实战、顺序号_java生成顺序增长序列,消息队列kafka面试题

最后的话

无论是哪家公司,都很重视Spring框架技术,重视基础,所以千万别小看任何知识。面试是一个双向选择的过程,不要抱着畏惧的心态去面试,不利于自己的发挥。
同时看中的应该不止薪资,还要看你是不是真的喜欢这家公司,好了希望这篇文章对大家有帮助!

部分截图:
在这里插入图片描述

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

/**
 * 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
    */
    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”);

最后

文章中涉及到的知识点我都已经整理成了资料,录制了视频供大家下载学习,诚意满满,希望可以帮助在这个行业发展的朋友,在论坛博客等地方少花些时间找资料,把有限的时间,真正花在学习上,所以我把这些资料,分享出来。相信对于已经工作和遇到技术瓶颈的朋友们,在这份资料中一定都有你需要的内容。

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

equence(prefixName,width,1);
SimpleDateFormat dateFormat=new SimpleDateFormat(“yyyyMMdd”);
String format = dateFormat.format(new Date());
return prefixName+“-”+format+“-”+newSequence;
}else{
SimpleDateFormat dateFormat=new SimpleDateFormat(“yyyyMMdd”);

最后

文章中涉及到的知识点我都已经整理成了资料,录制了视频供大家下载学习,诚意满满,希望可以帮助在这个行业发展的朋友,在论坛博客等地方少花些时间找资料,把有限的时间,真正花在学习上,所以我把这些资料,分享出来。相信对于已经工作和遇到技术瓶颈的朋友们,在这份资料中一定都有你需要的内容。

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  • 12
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
public synchronized String nextId() { long timestamp = timeGen(); //获取当前毫秒数 //如果服务器时间有问(时钟后退) 报错。 if (timestamp < lastTimestamp) { throw new RuntimeException(String.format( "Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); } //如果上次生成时间和当前时间相同,在同一毫秒内 if (lastTimestamp == timestamp) { //sequence自增,因为sequence只有12bit,所以和sequenceMask相与一下,去掉高位 sequence = (sequence + 1) & sequenceMask; //判断是否溢出,也就是每毫秒内超过4095,当为4096时,与sequenceMask相与,sequence就等于0 if (sequence == 0) { timestamp = tilNextMillis(lastTimestamp); //自旋等待到下一毫秒 } } else { sequence = 0L; //如果和上次生成时间不同,重置sequence,就是下一毫秒开始,sequence计数重新从0开始累加 } lastTimestamp = timestamp; long suffix = (datacenterId << datacenterIdShift) | (workerId << workerIdShift) | sequence; String datePrefix = DateFormatUtils.format(timestamp, "yyyyMMddHHMMssSSS"); return datePrefix + suffix; } protected long tilNextMillis(long lastTimestamp) { long timestamp = timeGen(); while (timestamp <= lastTimestamp) { timestamp = timeGen(); } return timestamp; } protected long timeGen() { return System.currentTimeMillis(); } private byte getLastIP(){ byte lastip = 0; try{ InetAddress ip = InetAddress.getLocalHost(); byte[] ipByte = ip.getAddress(); lastip = ipByte[ipByte.length - 1]; } catch (UnknownHostException e) { e.printStackTrace(); } return lastip; }

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值