目录
一、使用线程安全的类------AtomicInteger或者AtomicLong
一、使用线程安全的类------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个流水号
}
}



数据库结果:
