Java并发编程实战、顺序号

目录

一、功能介绍

二、并发安全以及性能优化

三、并发技术以及数据库表设计

四、思路和代码实现

五、源代码下载


一、功能介绍

        顺序号是项目开发中使用频率很高的基础服务之一,为每一个不同的业务数据生成一个唯一

的顺序号,一般为固定编码加日期,然后是顺序号。

        比如 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

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
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; }

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值