分布式主键生成服务

目录

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

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

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

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

(一)流水号概述

(二)添加配置

1.pom.xml

2.application.properties

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

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

1.基础代码

2.基础代码存在的问题 

3.基础代码优化 

4.使用httpd进行并发测试 

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

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


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

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
    }
}

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

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

<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: 

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

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

Controller: 

@RestController
public class PrimaryController {
    @Autowired
    private PrimaryService primaryService;

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

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

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

@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表示女

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

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位 

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

@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,这种生成方式就不能借助时间戳了,需要借助数据库来生成。

创建一张表:

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

<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

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,修改启动类

@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;
    }
}
public interface PkSeedMapper extends BaseMapper<PkSeed> {
}
@SpringBootApplication
@MapperScan("com.javatest.mapper")
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class,args);
    }
}

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

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

1.基础代码

PrimaryService类中添加:

// 流水号生成基础写法
@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);
}
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.基础代码优化 

@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,也应该由参数传入,这里简写了。

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

 yum -y install httpd

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

解释:

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

测试前:

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

测试后: 

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

[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,放到内存中

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;
}
@GetMapping("/getSerialNum")
public String callGetSerialNum(String type) {
    return primaryService.callGetSerialNum(type);
}

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

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

ab -c 100 -n 100 http://192.168.0.16:8080/getSerialNum?type=user

  

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

PrimaryController:

@GetMapping("/getCode")
public String getCode(String type, String parentCode, String rule) {
    return GradeCodeGenerator.getNextCode(type, parentCode, rule);
}
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;
    }

}
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");
    }
}
@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个流水号
    }
}

数据库结果: 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/978759.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux 环境“从零”部署 MongoDB 6.0:mongosh 安装与数据操作全攻略

前提 完成linux平台部署MongoDB【部署教程】且完成mongosh的安装 由于本人使用的是6.0版本的MongoDB&#xff0c;新版本 MongoDB&#xff08;尤其是 6.0 及以上版本&#xff09;已经不再默认捆绑传统的 mongo shell&#xff0c;而改用新的 MongoDB Shell&#xff08;mongosh&am…

使用Docker将ros1自定义消息通过rosjava_bootstrap生成jar包

文章目录 预准备环境rosjava_bootstrap坏消息好消息 环境安装docker安装rosjava_bootstrap仓库rosjava_center仓库修改rosjava_bootstrap代码拉取docker镜像放置自己的自定义消息 启动docker编译 预准备环境 rosjava_bootstrap rosjava_bootstrap是将自定义的ROS消息生成java…

RNN,LSTM,GRU三种循环网络的对比

1. 三种网络在准确率的对比 2. 三种网络在损失值的对比 3. 三种网络在计算时间的对比 4. RNN&#xff08;传统循环神经网络&#xff09; 主要特点&#xff1a; RNN 是最基础的循环神经网络&#xff0c;通过 递归 计算每个时间步的输出。在每个时间步&#xff0c;RNN 会将当前…

hackmyvm-hero

信息收集 ┌──(root㉿kali)-[/home/kali/Desktop/hackmyvm] └─# arp-scan -I eth1 192.168.56.0/24 Interface: eth1, type: EN10MB, MAC: 00:0c:29:34:da:f5, IPv4: 192.168.56.103 Starting arp-scan 1.10.0 with 256 hosts (https://github.com/royhills/arp-scan) 192…

纷析云:赋能企业财务数字化转型的开源解决方案

在企业数字化转型的浪潮中&#xff0c;财务管理的高效与安全成为关键。纷析云凭借其开源、安全、灵活的财务软件解决方案&#xff0c;为企业提供了一条理想的转型路径。 一、开源的力量&#xff1a;自主、安全、高效 纷析云的核心优势在于其100%开源的财务软件源码。这意味着…

异常c/c++

目录 1.c语言传统处理错误方式 1、终止程序 2、返回错误码 2.c异常概念 3.异常的使用 3.1异常的抛出与捕获 3.2异常安全&#xff08;还有一些异常重新抛出&#xff09; 3.3异常规范 4.自定义异常体系 5.c标准库的异常体系 6.异常优缺点 1、优点 2、缺点 7、补充 1.…

SAP-ABAP:使用ST05(SQL Trace)追踪结构字段来源的步骤

ST05 是 SAP 提供的 SQL 跟踪工具&#xff0c;可以记录程序运行期间所有数据库操作&#xff08;如 SELECT、UPDATE、INSERT&#xff09;。通过分析跟踪结果&#xff0c;可以精准定位程序中结构字段对应的数据库表。 步骤1&#xff1a;激活ST05跟踪 事务码 ST05 → 点击 Activa…

sklearn中的决策树-分类树:剪枝参数

剪枝参数 在不加限制的情况下&#xff0c;一棵决策树会生长到衡量不纯度的指标最优&#xff0c;或者没有更多的特征可用为止。这样的决策树 往往会过拟合。为了让决策树有更好的泛化性&#xff0c;我们要对决策树进行剪枝。剪枝策略对决策树的影响巨大&#xff0c;正确的剪枝策…

安宝特科技 | Vuzix Z100智能眼镜+AugmentOS:重新定义AI可穿戴设备的未来——从操作系统到硬件生态,如何掀起无感智能革命?

一、AugmentOS&#xff1a;AI可穿戴的“操作系统革命” 2025年2月3日&#xff0c;Vuzix与AI人机交互团队Mentra联合推出的AugmentOS&#xff0c;被业内视为智能眼镜领域的“iOS时刻”。这款全球首个专为智能眼镜设计的通用操作系统&#xff0c;通过三大突破重新定义了AI可穿戴…

基于Rook的Ceph云原生存储部署与实践指南(上)

#作者&#xff1a;任少近 文章目录 1 Ceph环境准备2 rook部署ceph群集2.1 Rook 帮助地址2.2 安装ceph2.3 获取csi镜像2.4 Master参加到osd2.5 设置默认存储 3 Rook部署云原生RBD块存储3.1 部署storageclass资源3.2 部署WordPress使用RBD3.3 WordPress访问 4 Rook部署云原生RGW…

2月27(信息差)

&#x1f30d;雷军超钟睒睒登顶中国首富 身家近4400亿元 &#x1f384;全球AI大混战升温&#xff01;超越Sora的阿里万相大模型开源 家用显卡都能跑 ✨小米15 Ultra、小米SU7 Ultra定档2月27日 雷军宣布&#xff1a;向超高端进发 1.刚刚&#xff01;DeepSeek硬核发布&#xff…

【Linux】文件系统深度解析:从基础到高级应用

&#x1f3ac; 个人主页&#xff1a;努力可抵万难 &#x1f4d6; 个人专栏&#xff1a;《C语法》《Linux系列》《数据结构及算法》 ⛰️ 路虽远&#xff0c;行则将至 目录 &#x1f4da;一、引言&#xff1a;文件系统的核心作用与历史演进 &#x1f4d6;1.文件系统的定义与功…

《Effective Objective-C》阅读笔记(中)

目录 接口与API设计 用前缀避免命名空间冲突 提供“全能初始化方法” 实现description方法 尽量使用不可变对象 使用清晰而协调的命名方式 方法命名 ​编辑类与协议命名 为私有方法名加前缀 理解OC错误模型 理解NSCopying协议 协议与分类 通过委托与数据源协议进行…

MongoDB—(一主、一从、一仲裁)副本集搭建

MongoDB集群介绍&#xff1a; MongoDB 副本集是由多个MongoDB实例组成的集群&#xff0c;其中包含一个主节点&#xff08;Primary&#xff09;和多个从节点&#xff08;Secondary&#xff09;&#xff0c;用于提供数据冗余和高可用性。以下是搭建 MongoDB 副本集的详细步骤&am…

【实战 ES】实战 Elasticsearch:快速上手与深度实践-1.3.1单节点安装(Docker与手动部署)

&#x1f449; 点击关注不迷路 &#x1f449; 点击关注不迷路 &#x1f449; 点击关注不迷路 文章大纲 10分钟快速部署Elasticsearch单节点环境1. 系统环境要求1.1 硬件配置推荐1.2 软件依赖 2. Docker部署方案2.1 部署流程2.2 参数说明2.3 性能优化建议 3. 手动部署方案3.1 安…

Rt-thread源码剖析(1)——内核对象

前言 该系列基于rtthread-nano的内核源码&#xff0c;来研究RTOS的底层逻辑&#xff0c;本文介绍RTT的内核对象&#xff0c;对于其他RTOS来说也可供参考&#xff0c;万变不离其宗&#xff0c;大家都是互相借鉴&#xff0c;实现不会差太多。 内核对象容器 首先要明确的一点是什…

html css js网页制作成品——HTML+CSS甜品店网页设计(5页)附源码

目录 一、&#x1f468;‍&#x1f393;网站题目 二、✍️网站描述 三、&#x1f4da;网站介绍 四、&#x1f310;网站效果 五、&#x1fa93; 代码实现 &#x1f9f1;HTML 六、&#x1f947; 如何让学习不再盲目 七、&#x1f381;更多干货 一、&#x1f468;‍&#x1f…

Trae根据原型设计稿生成微信小程序密码输入框的踩坑记录

一、需求描述 最近经常使用Trae生成一些小组件和功能代码&#xff08;对Trae赶兴趣的可以看之前的文章《TraeAi上手体验》&#xff09;&#xff0c;刚好在用uniapp开发微信小程序时需要开发一个输入密码的弹框组件&#xff0c;于是想用Trae来实现。原型设计稿如下&#xff1a;…

斩波放大器

目录 简介 自稳零斩波放大器 噪声 简介 双极性放大器的失调电压为25 μV&#xff0c;漂移为0.1 μV/C。斩波放大器尽管存在一些不利影 响&#xff0c;但可提供低于5 μV的失调电压&#xff0c;而且不会出现明显的失调漂移&#xff0c; 以下图1给出了基本的斩波放大器电路图。…

windows设置暂停更新时长

windows设置暂停更新时长 win11与win10修改注册表操作一致 &#xff0c;系统界面不同 1.打开注册表 2.在以下路径 \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings 右键新建 DWORD 32位值&#xff0c;名称为FlightSettingsMaxPauseDays 根据需求填写数…