分布式主键生成服务

目录

一、使用线程安全的类------AtomicInteger或者AtomicLong

二、主键生成最简单写法(不推荐)

三、主键生成方法一:Long型id生成------雪花算法

四、主键生成方法二:流水号

(一)流水号概述

(二)添加配置

1.pom.xml

2.application.properties

3.创建实体类、Mapper,修改启动类

(三)流水号生成基础写法

1.基础代码

2.基础代码存在的问题

3.基础代码优化

4.使用httpd进行并发测试

(四)流水号生成批量拿取id

五、主键生成方法三:级次编码


一、使用线程安全的类------AtomicInteger或者AtomicLong

java 复制代码
import java.util.concurrent.atomic.AtomicInteger;

public class TestService {

    public Integer count = 0;
    // 多个线程同时对同一个数字进行操作的时候,使用AtomicInteger或者AtomicLong
    public AtomicInteger atomicInteger = new AtomicInteger(0);


    public static void main(String[] args) {
        // 创建两个线程,每个线程对count进行10000次自增操作
        TestService testService = new TestService();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
//                primaryService.count++;
//                primaryService.atomicInteger.incrementAndGet();
                testService.atomicInteger.getAndIncrement();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
//                primaryService.count++;
//                primaryService.atomicInteger.incrementAndGet();
                testService.atomicInteger.getAndIncrement();
            }
        });
        t1.start();
        t2.start();
        // 使用 t1.join() 和 t2.join() 来确保主线程
        // (即 main 方法所在的线程)会等待 t1 和 t2 执行完毕后再继续执行
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
//        System.out.println("count=" + primaryService.count); // count=10671

        // 如果你只是关心最终的 count 值,而不是每次操作的具体返回值,那么 incrementAndGet() 和 getAndIncrement() 的结果看起来是一样的,因为它们都会正确地对 count 进行自增操作。但是,
        // 如果你需要依赖返回值来进行某些逻辑判断或处理,那么选择哪个方法就很重要了。
        System.out.println("atomicInteger=" + testService.atomicInteger); // count=10671
    }
}

二、主键生成最简单写法(不推荐)

创建一个项目,添加依赖:

XML 复制代码
<parent>
    <artifactId>spring-boot-starter-parent</artifactId>
    <groupId>org.springframework.boot</groupId>
    <version>2.6.11</version>
</parent>
<dependencies>    
    <!--注意:所有的依赖不可以随意添加,不需要的依赖不要添加,宁可少不能多-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>
oject>

Service:

java 复制代码
@Service
public class PrimaryService {
    // 生成主键最简单方式
    private AtomicLong num = new AtomicLong(0);

    public Long getId() {
        return num.incrementAndGet();
    }
}

Controller:

java 复制代码
@RestController
public class PrimaryController {
    @Autowired
    private PrimaryService primaryService;

    @GetMapping("/getId")
    public Long getId() {
        return primaryService.getId();
    }
}

存在的问题:该方法虽然能够保证多线程下的安全性,但是每次重启服务都会从1开始获取。

**因此,**new AtomicLong(0)的初始值不应该设置为0,可以设置为当前毫秒值。正确写法:

java 复制代码
@Service
public class PrimaryService implements InitializingBean {
    // 生成主键最简单方式
    private AtomicLong num;

    // 初始化Bean对象中的属性
    @Override
    public void afterPropertiesSet() throws Exception {
        num = new AtomicLong(System.currentTimeMillis());
    }

    public Long getId() {
        return num.incrementAndGet();
    }
}

无论重启多少次,拿到的id永远是越来越大。

但是使用时间戳作为主键也有问题:

当主键生成服务部署成集群时,启动主键生成服务集群,生成的时间戳是一样的,这样当user服务第一次拿取和第二次拿取的主键值很有可能一样。针对这一问题的解决方案就是:雪花算法。

三、主键生成方法一:Long型id生成------雪花算法

雪花算法的含义:用不同的,表示不同的含义。

1个字节是8位,以int类型为例,该数据类型是4个字节,所以是4*8=32位。

long类型是8个字节,就是8*8=64位

例如:

0 1000000 00000000 00000000 00000000

第一位表示符号位 第2位如果是0表示男,如果是1表示女

这里,我们将时间戳转为二进制:

java 复制代码
public static void main(String[] args) {
    System.out.println(Long.toBinaryString(1740485559705L));
}

结果:

1 10010101 00111101 00000110 00000101 10011001

不够64位,左边补0:

00000000 00000000 00000001 10010101 00111101 00000110 00000101 10011001

使用雪花算法分成三段:

第1位是符号位,0表示正数

第2位到第7位表示机器号,如果都占满,则为1111111,转为十进制为:127台机器

一个普通的微服务架构,最多127台机器已经足够了

剩下的56位,表示当前时间,一个 56 位时间戳(以毫秒为单位,从 Unix 纪元开始计时)最多能表示到大约 公元 3020 年 左右。如果今天是 2025 年 2 月 25 日,则从今天开始,56 位时间戳可以覆盖未来约 千年的时间范围,也足够了。

其他的雪花算法形式:

位运算Tips:

按位或(|) :当两个相应的二进制位中至少有一个是1时,结果为1;否则为0。

例如:0101 | 0111 = 0111

按位异或(^):当两个相应的二进制位不同时,结果为1;相同则为0。

例如:0101 ^ 0111 = 0010

以下图的时间戳为例:如果机器号为1,那么先将机器号左移56位

左移后的数据,与当前时间戳进行位运算,最终,机器号和时间戳就可以成功拼接。

java 复制代码
@Service
public class PrimaryService implements InitializingBean {
    // 生成主键最简单方式
    private AtomicLong baseId;

    // 将机器号作为配置
    @Value("${node.id:1}") // 表示配置文件中node.id的值,如果没有配置,则默认为1
    private long nodeId;

    // afterPropertiesSet方法初始化Bean对象中的属性
    @Override
    public void afterPropertiesSet() throws Exception {
        long now = System.currentTimeMillis();
        // 机器号先左移56位,再和时间戳做或运算,就是初始id值
        baseId = new AtomicLong(nodeId << 56 | now);
    }

    public Long getId() {
        return baseId.incrementAndGet();
    }
}

http://localhost:8080/getId结果:72059334530093401

返回的Id不宜过大,因为前端的js的number无法容纳这么长的值,因此需要转为字符串返回给前端,否则会精度丢失。

四、主键生成方法二:流水号

(一)流水号概述

流水号示例:000001、000002、000003或者202502250001、202502250002

流水号的生成必须按顺序来,第一次生成的是000001,那么第二次生成的必须是000002,这种生成方式就不能借助时间戳了,需要借助数据库来生成。

创建一张表:

sql 复制代码
create table if not exists pk.`pk|_seed`
(
    type    varchar(30) not null comment '业务类型:user,contract,order'
        primary key,
    value   int         null comment '最新值',
    version int         null comment '版本号,做乐观锁'
);

type表示不同的业务类型

(二)添加配置

1.pom.xml

XML 复制代码
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.2</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.29</version>
</dependency>

注意:当下载依赖的时候,依赖树中存在依赖,但是左侧的依赖没有,重启IDEA即可,这是IDEA的bug。

2.application.properties

XML 复制代码
server.port=8080
#数据库
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.157.102:3306/pk?useSSL=false
spring.datasource.username=root
spring.datasource.password=123456

3.创建实体类、Mapper,修改启动类

java 复制代码
@TableName("pk_seed")
public class PkSeed {
    @TableId // 标识该字段是主键
    private String type;
    private Integer value;
    private Integer version;

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public Integer getValue() {
        return value;
    }

    public void setValue(Integer value) {
        this.value = value;
    }

    public Integer getVersion() {
        return version;
    }

    public void setVersion(Integer version) {
        this.version = version;
    }
}
java 复制代码
public interface PkSeedMapper extends BaseMapper<PkSeed> {
}
java 复制代码
@SpringBootApplication
@MapperScan("com.javatest.mapper")
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class,args);
    }
}

我们不使用mybatisplus自带的雪花算法,而是自定义,可以控制某一个位是什么含义。

(三)流水号生成基础写法

1.基础代码

PrimaryService类中添加:

java 复制代码
// 流水号生成基础写法
@Autowired
private PkSeedMapper pkSeedMapper;
public String getSerialNum(String type) {
    if (!StringUtils.hasText(type)) {
        throw new RuntimeException("type is null");
    }
    int result = 0;
    PkSeed pkSeed = pkSeedMapper.selectById(type);
    if (pkSeed == null) {
        pkSeed = new PkSeed();
        pkSeed.setType(type);
        pkSeed.setValue(1);
        pkSeed.setVersion(1);
        pkSeedMapper.insert(pkSeed);
        result = pkSeed.getValue(); // 返回最新的值
    } else {
        // 如果根据主键查询到,则更新
        result = pkSeed.getValue() + 1;
        pkSeedMapper.updateValue(result, type, pkSeed.getVersion());
    }
    return String.format("%06d", result);
}
java 复制代码
public interface PkSeedMapper extends BaseMapper<PkSeed> {
    @Update("update pk_seed set value = #{value} , version = version + 1 where type = #{type} and version = #{version}")
    void updateValue(int value, String type, int version);
}

重启服务,浏览器访问:

user服务:

order也是一样:

2.基础代码存在的问题

上述代码存在的问题:多线程问题。

当有两个线程同时执行更新语句updateValue方法时,A线程和B线程同时查到version=5,A线程先执行,version改为6,接下来B线程后执行,因为B线程要根据version=5的条件修改value,但是version此时已经是6了,所以会执行失败。这就是数据库乐观锁

失败会直接中断程序,我们可以将失败的情况进行循环。

3.基础代码优化

java 复制代码
@Autowired
private PkSeedMapper pkSeedMapper;
public String getSerialNum(String type) {
    if (!StringUtils.hasText(type)) {
        throw new RuntimeException("type is null");
    }
    // 流水号的最新值
    int result = 0;
    int updateCount = 0;
    int times = 0;
    do {
        if (times > 10) {
            throw new RuntimeException("服务器繁忙,请稍候再试");
        }
        PkSeed pkSeed = pkSeedMapper.selectById(type);
        if (pkSeed == null) {
            pkSeed = new PkSeed();
            pkSeed.setType(type);
            pkSeed.setValue(1);
            pkSeed.setVersion(1);
            pkSeedMapper.insert(pkSeed);
            result = pkSeed.getValue(); // 返回最新的值
            updateCount = 1;
        } else {
            // 如果根据主键查询到,则更新
            updateCount = pkSeedMapper.updateValue(pkSeed.getValue() + 1, type, pkSeed.getVersion());
            if (updateCount == 1) {
                result = pkSeed.getValue() + 1;
            }
        }
        // 每次更新都增加一次次数
        times++;
    } while (updateCount == 0); // 更新失败,重新执行循环
    return String.format("%06d", result);
}

注意:这里流水号的长度,也就是String.format("%06d", result);中的6,也应该由参数传入,这里简写了。

java 复制代码
public interface PkSeedMapper extends BaseMapper<PkSeed> {
    @Update("update pk_seed set value = #{value} , version = version +1 where type = #{type} and version = #{version}")
    int updateValue(int value, String type, int version);
}

4.使用httpd进行并发测试

安装一个并发测试工具:httpd

java 复制代码
 yum -y install httpd

使用ab测试: ab -c 10 -n 10 http://localhost/

解释:

  • -c 10:一次发送10个请求
  • -n 10:请求的总数量是10
  • -c 10 -n 100:表示发送测试10次,每次并发10个请求

测试前:

java 复制代码
[root@localhost soft]# ab -c 10 -n 10 http://192.168.0.16:8080/getSerialNum?type=user

测试后:

一次性使用50个并发请求:

java 复制代码
[root@localhost soft]# ab -c 50 -n 50 http://192.168.0.16:8080/getSerialNum?type=user

因此,优化后的也有线程问题。我们继续进行优化。

(四)流水号生成批量拿取id

根据上面的代码,在同时有更多请求时,虽然使用了数据库乐观锁保证了数据的安全,但是不能保证所有的sql都执行成功。每次从数据库获取id,都要进行IO操作,且不一定每次都成功,如果添加100个id,就要修改100次数据库,效率低下

优化思路:每次从数据库拿到200个id,把这200个数字放到内存中,之后的请求都可以从内存中拿到id,不需要修改数据库。每次拿1个,数据库中的id就+1,以此类推,每次从数据库拿200个,数据库中的id就+200。

200就是最新值,也就是最大值。根据最大值可以计算出最小值,min=max-num+1,num就是要获取的id个数,当前最大值和要获取的id个数都是200,那么min=200-200+1=1。拿到了最大值和最小值,中间的200个数字就能计算出来了。

以此类推,当id初始值为1684时,要一次性从数据库中拿412个id,那么最大值就是1684+412=2096,最小值=2096-412+1=1685。拿到了最大值和最小值,中间的412个数字就能计算出来了。

这412个数字不需要都放在内存中,只需要将最大值和最小值放在map中即可,直接查询内存即可。每次拿取最小值都+1,直到最小值>最大值,说明内存中累计存放的412个数字用完了,再去数据库中一次性取412个id,循环往复。

每次从数据库拿到200个id,放到内存中,具体操作是:

a. 每次让数据库中的value+200,得到一个最大的id = max

b. 根据 max 计算出 最小的id, 计算公式 min = max - count + 1

c. 把最大 id 和最小 id 放到 map 中,

d. 如果有人请求获取id,先判断内存中是否有,如果有就从内存获取,

每次获取都让 min++,直到 min 的值 > max时,下一次请求,再次从数据库中查出200个id,放到内存中

java 复制代码
private static final int count = 200; // 一次性从数据库获取的流水号数量
// 最大id的map key是type,value是当前type的最大id
private static ConcurrentHashMap<String, AtomicLong> maxIdMap = new ConcurrentHashMap<>();
// 最小id的map
private static ConcurrentHashMap<String, AtomicLong> minIdMap = new ConcurrentHashMap<>();
public String callGetSerialNum(String type) {
    AtomicLong maxValue = maxIdMap.get(type);
    AtomicLong minValue = minIdMap.get(type);
    synchronized (type.intern()) {
        if (minValue == null || maxValue == null || minValue.get() > maxValue.get()) {
            // 从数据库中获取新的值=最大值
            long max = getSerialNum(type, count);
            // 计算最小值
            long min = max - count + 1;
            minIdMap.put(type, new AtomicLong(min));
            maxIdMap.put(type, new AtomicLong(max));
        }
        return String.format("%06d", minIdMap.get(type).getAndIncrement()); // 返回给前端最小值
    }
}
// 流水号生成基础写法
@Autowired
private PkSeedMapper pkSeedMapper;
public long getSerialNum(String type, int count) {
    if (!StringUtils.hasText(type)) {
        throw new RuntimeException("type is null");
    }
    // 流水号的最新值
    int result = 0;
    int updateCount = 0;
    int times = 0;
    do {
        if (times > 10) {
            throw new RuntimeException("服务器繁忙,请稍候再试");
        }
        PkSeed pkSeed = pkSeedMapper.selectById(type);
        if (pkSeed == null) {
            pkSeed = new PkSeed();
            pkSeed.setType(type);
            pkSeed.setValue(count);
            pkSeed.setVersion(1);
            pkSeedMapper.insert(pkSeed);
            result = pkSeed.getValue(); // 返回最新的值
            updateCount = 1;
        } else {
            // 如果根据主键查询到,则更新
            updateCount = pkSeedMapper.updateValue(pkSeed.getValue() + count, type, pkSeed.getVersion());
            if (updateCount == 1) {
                result = pkSeed.getValue() + count;
            }
        }
        // 每次更新都增加一次次数
        times++;
    } while (updateCount == 0); // 更新失败,重新执行循环
    return result;
}
java 复制代码
@GetMapping("/getSerialNum")
public String callGetSerialNum(String type) {
    return primaryService.callGetSerialNum(type);
}

删除user那一行:重启项目,浏览器访问

高并发请求也没有错误,并且时间更短了

java 复制代码
ab -c 100 -n 100 http://192.168.0.16:8080/getSerialNum?type=user

五、主键生成方法三:级次编码

PrimaryController:

java 复制代码
@GetMapping("/getCode")
public String getCode(String type, String parentCode, String rule) {
    return GradeCodeGenerator.getNextCode(type, parentCode, rule);
}
java 复制代码
import com.javatest.domain.GradeCodeRule;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * 级次编码生成工具
 */
public class GradeCodeGenerator {
    //级次编码规则缓存
    private static ConcurrentMap<String, GradeCodeRule> codeRuleMap = new ConcurrentHashMap<>();

    static PrimaryService primaryService;

    /**
     * 例如:2|2|2|2
     * 根据父编码获取下一个子编码
     * parentCode=01            第一次调用返回:0101,第二次调用返回:0102
     * parentCode=null || ""    第一次调用返回:01,第二次调用返回:02
     *
     * @param codeRule   编码规则
     * @param parentCode 父编码 可以为空
     * @param type       编码类型:业务名称
     * @return
     */
    public static String getNextCode(String type, String parentCode, String codeRule) {
        if (!codeRuleMap.containsKey(codeRule)) {
            codeRuleMap.putIfAbsent(codeRule, new GradeCodeRule(codeRule));
        }
        return createCode(codeRuleMap.get(codeRule), parentCode, type);
    }

    private static String createCode(GradeCodeRule gradeCodeRule, String parentCode, String type) {
        parentCode = parentCode == null || parentCode.length() == 0 ? "" : parentCode;
        int parentGrade = gradeCodeRule.getCodeGrade(parentCode);//父编码级次
        if (parentGrade < 0) {
            throw new IllegalArgumentException("parentCode(" + parentCode + ")跟codeRule(" + gradeCodeRule.getRule() + ")不匹配");
        }
        if (parentGrade >= gradeCodeRule.getMaxGradeCount()) {
            throw new IllegalArgumentException(parentCode + "已经是最末级编码,无法获取子编码!");
        }
//        int parentGradeLength = gradeCodeRule.getGradeLength(parentGrade);
        int subCodeSectionLength = gradeCodeRule.getSectionLength(parentGrade + 1);//子编码末级片段长度
        long nextSubCode = primaryService.getLongKey(type + "-" + gradeCodeRule.getRule() + "-" + parentCode);
        String nextSubCodeStr = String.format("%0" + subCodeSectionLength + "d", nextSubCode);
        if (nextSubCodeStr.length() > subCodeSectionLength) {
            throw new IllegalArgumentException(parentCode + "的下一级编码已经用完!");
        }
        StringBuilder codeBuilder = new StringBuilder(parentCode);
        codeBuilder.append(nextSubCodeStr);
        return codeBuilder.toString();
    }

    static void setPrimaryKeyService(PrimaryService ps) {
        primaryService = ps;
    }

}
java 复制代码
package com.javatest.domain;

import java.util.Arrays;
import java.util.StringTokenizer;

/**
 * 级次编码规则实体
 */
public class GradeCodeRule {

    private static final String regex = "([1-9]\\d{0,}\\|{0,1})+";

    /**
     * 编码规则,例:2|2|2
     * 表示:一共有3级,每级的长度为2
     */
    private String rule;
    /**
     * 各级分段长度,例:2|2|2
     * 则:[2,2,2]
     */
    private int[] codeSection = null;
    /**
     * 各级编码长度,例:2|2|2
     * 则:[2,4,6]
     */
    private int[] codeLength = null;
    /**完整编码长度*/
    private int fullGradeLength;

    public GradeCodeRule(String rule){
        checkRule(rule);
        this.rule = rule;
        parseRule(rule);
    }

    private void parseRule(String rule) {
        StringTokenizer st = new StringTokenizer(rule,"|");
        int count = st.countTokens();
        codeSection = new int[count];
        codeLength = new int[count];
        int index = 0;
        fullGradeLength = 0;
        try {
            while (st.hasMoreTokens()) {
                codeSection[index] = Integer.parseInt(st.nextToken());
                fullGradeLength += codeSection[index];
                codeLength[index++] = fullGradeLength;
            }
        } catch (Exception e) {
            throw new IllegalArgumentException("请提供正确的级次编码规则,例如:2|2|2|2,表示:一共有4级,每级的长度为2");
        }
    }

    /**
     * 得到编码第grade级片段的长度。<br/>
     * 如编码规则为"2/3/2/3",则:<br/>
     * 2级编码片段长度为3,<br/>
     * 3级编码片段长度为2。<br/>
     * @param grade int 1:代表第一级,依次类推
     * @return int
     */
    public int getSectionLength(int grade) {
        if (grade <= 0) {
            return 0;
        } else {
            return codeSection[--grade];
        }
    }

    /**
     * 获取编码级次
     * 如编码规则为"2|2|2",则:<br>
     * 编码"1010"的级次为2,<br>
     * 编码"10101010"的级次为-1,表示code跟编码规则不匹配,<br>
     * @param code
     * @return 编码级次
     */
    public int getCodeGrade(String code) {
        if(code==null || code.length()==0) return 0;
        int index = Arrays.binarySearch(codeLength, code.length());
        return index >=0 ? index+1 : -1;
    }

    /**
     * 验证编码规则是否符合规范
     * @param rule
     */
    public void checkRule(String rule) {
        if(!rule.matches(regex)){
            throw new IllegalArgumentException(rule+":不正确,请提供正确的级次编码规则,例如:2|2|2|2,表示:一共有4级,每级的长度为2");
        }
    }

    /**
     * 得到最大编码级次
     * @return 最大编码级次
     */
    public int getMaxGradeCount(){
        return codeSection.length;
    }

    public String getRule() {
        return rule;
    }

    public void setRule(String rule) {
        this.rule = rule;
    }

    public static void main(String[] args) {
        new GradeCodeRule("2,3,4");
    }
}
java 复制代码
@Service
public class PrimaryService implements InitializingBean {
    // 初始id值
    private AtomicLong baseId;

    // 将机器号作为配置
    @Value("${node.id:1}") // 表示配置文件中node.id的值,如果没有配置,则默认为1
    private long nodeId;

    // afterPropertiesSet方法初始化Bean对象中的属性
    @Override
    public void afterPropertiesSet() throws Exception {
        long now = System.currentTimeMillis();
        // 机器号先左移56位,再和时间戳做或运算,就是初始id值
        baseId = new AtomicLong(nodeId << 56 | now);
        GradeCodeGenerator.setPrimaryKeyService(this); // todo
    }

    public Long getId() {
        return baseId.incrementAndGet();
    }

    private static final int count = 200; // 一次性从数据库获取的流水号数量

    // 最大id的map key是type,value是当前type的最大id
    private static ConcurrentHashMap<String, AtomicLong> maxIdMap = new ConcurrentHashMap<>();
    // 最小id的map
    private static ConcurrentHashMap<String, AtomicLong> minIdMap = new ConcurrentHashMap<>();

    public String callGetSerialNum(String type) {
        AtomicLong maxValue = maxIdMap.get(type);
        AtomicLong minValue = minIdMap.get(type);

        synchronized (type.intern()) {
            if (minValue == null || maxValue == null || minValue.get() > maxValue.get()) {
                // 从数据库中获取新的值=最大值
                long max = getSerialNum(type, count);
                // 计算最小值
                long min = max - count + 1;
                minIdMap.put(type, new AtomicLong(min));
                maxIdMap.put(type, new AtomicLong(max));
            }
            return String.format("%06d", minIdMap.get(type).getAndIncrement()); // 返回给前端最小值
        }
    }

    // 流水号生成基础写法
    @Autowired
    private PkSeedMapper pkSeedMapper;

    public long getSerialNum(String type, int count) {
        if (!StringUtils.hasText(type)) {
            throw new RuntimeException("type is null");
        }

        // 流水号的最新值
        int result = 0;
        int updateCount = 0;
        int times = 0;
        do {
            if (times > 10) {
                throw new RuntimeException("服务器繁忙,请稍候再试");
            }
            PkSeed pkSeed = pkSeedMapper.selectById(type);
            if (pkSeed == null) {
                pkSeed = new PkSeed();
                pkSeed.setType(type);
                pkSeed.setValue(count);
                pkSeed.setVersion(1);
                pkSeedMapper.insert(pkSeed);
                result = pkSeed.getValue(); // 返回最新的值
                updateCount = 1;
            } else {
                // 如果根据主键查询到,则更新
                updateCount = pkSeedMapper.updateValue(pkSeed.getValue() + count, type, pkSeed.getVersion());
                if (updateCount == 1) {
                    result = pkSeed.getValue() + count;
                }
            }
            // 每次更新都增加一次次数
            times++;
        } while (updateCount == 0); // 更新失败,重新执行循环

        return result;
    }

    // 生成级次编码需要借助流水号的方法
    public long getLongKey(String type) {
        return getSerialNum(type, 1); // 1表示每次从数据库中 获取1个流水号
    }
}

数据库结果:

相关推荐
boy快快长大10 分钟前
Linux: 已占用接口
linux·运维·服务器
m0_7482554118 分钟前
适用于IntelliJ IDEA 2024.1.2部署Tomcat的完整方法,以及笔者踩的坑,避免高血压,保姆级教程
java·tomcat·intellij-idea
奋斗的小方19 分钟前
Springboot基础篇(3):Bean管理
java·spring boot·后端
网硕互联的小客服25 分钟前
服务器硬件老化导致性能下降的排查与优化
linux·运维·服务器·安全·centos
容器( ु⁎ᴗ_ᴗ⁎)ु.。oO25 分钟前
仿12306购票系统(3)
java·前端
新时代丘鸣山34 分钟前
Idea java项目结构介绍
java·ide·intellij-idea
tianmu_sama1 小时前
[Linux高性能服务器编程] 多线程编程
linux·服务器·c++·算法
2403_875180951 小时前
AI数字人开发,引领科技新潮流
java·大数据·数据结构·人工智能·科技·前端框架
扫地僧0091 小时前
JVM 高级面试题及答案整理,最新面试题
java·jvm·面试
2401_882726481 小时前
组态软件在物联网中的应用
java·物联网·struts·低代码·web