当前位置: 首页 > news >正文

分布式主键生成服务

目录

一、使用线程安全的类——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或者AtomicLongpublic 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 {@Autowiredprivate 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对象中的属性@Overridepublic 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的值,如果没有配置,则默认为1private long nodeId;// afterPropertiesSet方法初始化Bean对象中的属性@Overridepublic 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的值,如果没有配置,则默认为1private long nodeId;// afterPropertiesSet方法初始化Bean对象中的属性@Overridepublic 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的最大idprivate static ConcurrentHashMap<String, AtomicLong> maxIdMap = new ConcurrentHashMap<>();// 最小id的mapprivate 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()); // 返回给前端最小值}}// 流水号生成基础写法@Autowiredprivate 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个流水号}
}

数据库结果: 

相关文章:

分布式主键生成服务

目录 一、使用线程安全的类——AtomicInteger或者AtomicLong 二、主键生成最简单写法(不推荐) 三、主键生成方法一&#xff1a;Long型id生成——雪花算法 四、主键生成方法二&#xff1a;流水号 (一)流水号概述 (二)添加配置 1.pom.xml 2.application.properties 3.创…...

如何通过网管提升运维效率?

网络系统在企业信息化系统扮演着越来越重要的作用&#xff0c;网络规模不断扩大&#xff0c;网络结构越来越复杂&#xff0c;传统的运维方式已经难以满足高效、稳定运行的要求。网管系统作为IT运维的重要工具&#xff0c;能够帮助企业实现网络的智能化管理&#xff0c;显著提升…...

(python)Arrow库使时间处理变得更简单

前言 Arrow库并不是简单的二次开发,而是在datetime的基础上进行了扩展和增强。它通过提供更简洁的API、强大的时区支持、丰富的格式化和解析功能以及人性化的显示,填补了datetime在某些功能上的空白。如果你需要更高效、更人性化的日期时间处理方式,Arrow库是一个不错的选择…...

机器学习数学基础:33.分半信度

分半信度&#xff08;Split-Half Reliability&#xff09;深度教程 专为零基础小白打造&#xff0c;全面掌握分半信度知识 一、深入理解分半信度 分半信度是一种用于评估测验内部一致性的重要方法&#xff0c;其核心思路在于将一个完整的测验拆分成两个部分&#xff0c;然后通…...

PyTorch 源码学习:GPU 内存管理之深入分析 CUDACachingAllocator

因引入 expandable_segments 机制&#xff0c;PyTorch 2.1.0 版本发生了较大变化。本文关注的是 PyTorch 原生的 GPU 内存管理机制&#xff0c;故研究的 PyTorch 版本为 2.0.0。代码地址&#xff1a; c10/cuda/CUDACachingAllocator.hc10/cuda/CUDACachingAllocator.cpp 更多内…...

0—QT ui界面一览

2025.2.26&#xff0c;感谢gpt4 1.控件盒子 1. Layouts&#xff08;布局&#xff09; 布局控件用于组织界面上的控件&#xff0c;确保它们的位置和排列方式合理。 Vertical Layout&#xff08;垂直布局&#xff09; &#xff1a;将控件按垂直方向排列。 建议&#xff1a;适…...

Jenkinsfile流水线构建教程

前言 Jenkins 是目前使用非常广泛的自动化流程的执行工具, 我们目前的一些自动化编译, 自动化测试都允许在 Jenkins 上面. 在 Jenkins 的术语里面, 一些自动化工作联合起来称之为流水线, 比如拉取代码, 编译, 运行自动化测试等. 本文的主要目的是引导你快速熟悉 Jenkinsfile …...

flex布局自定义一行几栏,靠左对齐===grid布局

模板 <div class"content"><div class"item">1222</div><div class"item">1222</div><div class"item">1222</div><div class"item">1222</div><div class"…...

开发HarmonyOS NEXT版五子棋游戏实战

大家好&#xff0c;我是 V 哥。首先要公布一个好消息&#xff0c;V 哥原创的《鸿蒙HarmonyOS NEXT 开发之路 卷1&#xff1a;ArkTS 语言篇》图书终于出版了&#xff0c;有正在学习鸿蒙的兄弟可以关注一下&#xff0c;写书真是磨人&#xff0c;耗时半年之久&#xff0c;感概一下…...

AI革命下的多元生态:DeepSeek、ChatGPT、XAI、文心一言与通义千问的行业渗透与场景重构

前言 人工智能技术的爆发式发展催生了多样化的AI模型生态&#xff0c;从通用对话到垂直领域应用&#xff0c;从数据挖掘到创意生成&#xff0c;各模型凭借其独特的技术优势与场景适配性&#xff0c;正在重塑全球产业格局。本文将以DeepSeek、ChatGPT、XAI&#xff08;可解释人…...

大语言模型(LLM)微调技术笔记

图1&#xff1a;大模型进化树2 大模型微调 在预训练后&#xff0c;大模型可以获得解决各种任务的通用能力。然而&#xff0c;越来越多的研究表明&#xff0c;大语言模型的能力可以根据特定目标进一步调整。 这就是微调技术&#xff0c;目前主要有两种微调大模型的方法1&…...

六十天前端强化训练之第一天HTML5语义化标签深度解析与博客搭建实战

欢迎来到编程星辰海的博客讲解 目录 一、语义化标签的核心价值 1.1 什么是语义化&#xff1f; 1.2 核心优势 二、语义标签详解与使用场景 2.1 布局容器标签 2.2 内容组织标签 三、博客结构搭建实战 3.1 完整HTML结构 3.2 核心结构解析 3.3 实现效果说明 四、学习要点…...

NLP的预处理数据

处理文本数据的主要工具是Tokenizer。Tokenizer根据一组规则将文本拆分为tokens。然后将这些tokens转换为数字&#xff0c;然后转换为张量&#xff0c;成为模型的输入。模型所需的任何附加输入都由Tokenizer添加。 如果您计划使用预训练模型&#xff0c;重要的是使用与之关联的…...

【DeepSeek问答】QProcess::start是异步的吗?会使UI卡顿吗?

好的&#xff0c;我现在需要回答用户关于QProcess::start是否是异步的&#xff0c;以及它是否会导致UI卡顿的问题。用户之前已经询问过如何开发一个调用7-Zip的Qt应用&#xff0c;并且我给出了示例代码。现在用户可能在使用过程中遇到了问题&#xff0c;或者想更深入了解QProce…...

【Java项目】基于Spring Boot的体质测试数据分析及可视化设计

【Java项目】基于Spring Boot的体质测试数据分析及可视化设计 技术简介&#xff1a;采用Java技术、Spring Boot框架、MySQL数据库等实现。 系统简介&#xff1a;体质测试数据分析及可视化设计是一个基于Web的在线平台&#xff0c;主要分为前台和后台两大功能模块。前台功能模…...

JAVA-如何理解Mysql的索引

一、索引的概念 索引是一种特殊的文件&#xff0c;包含着对数据表里所有记录的引用(指针/地址)。可以对表中的一列或多列创建索引&#xff0c; 并指定索引的类型&#xff0c;各类索引有各自的数据结构实现。 二、索引是什么&#xff0c;用来干嘛 数据库中的表、数据、索引之间的…...

VUE向外暴露文件,并通过本地接口调用获取,前端自己生成接口获取public目录里面的文件

VUE中&#xff0c;如果我们想对外暴露一个文件&#xff0c;可以在打包之后也能事实对其进行替换&#xff0c;我们只需要把相关文件放置在public目录下即可&#xff0c;可以放置JSON&#xff0c;Excel等文件 比如我在这里放置一个other文件 我们可以直接在VUE中使用axios去获取…...

京准电钟:NTP精密时钟服务器在自动化系统中的作用

京准电钟&#xff1a;NTP精密时钟服务器在自动化系统中的作用 京准电钟&#xff1a;NTP精密时钟服务器在自动化系统中的作用 NTP精密时钟服务器在自动化系统中的作用非常重要&#xff0c;特别是在需要高精度时间同步的场景中。NTP能够提供毫秒级的时间同步精度&#xff0c;这…...

CSDN年度评选揭晓,永洪科技AI技术与智能应用双星闪耀

近日&#xff0c;永洪科技在CSDN&#xff08;中国专业开发者社区&#xff09;的年度评选中&#xff0c;凭借在人工智能技术创新与vividime在行业应用中的卓越表现&#xff0c;一举斩获“人工智能企业”及“智能应用”双料大奖。这一荣誉不仅彰显了永洪科技在AI领域的领先地位&a…...

vscode settings(二):文件资源管理器编辑功能主题快捷键

参考资料 Visual Studio Code权威指南 by 韩骏 一. 文件资源管理器 1.1 文件资源管理器隐藏文件夹 默认情况下&#xff0c;Visual Studio Code会在文件资源管理器中隐藏一些文件夹&#xff08;如.git文件夹&#xff09;​。可以通过files.exclude来配置要被隐藏的文件和文件…...

如何为服务器生成TLS证书

TLS&#xff08;Transport Layer Security&#xff09;证书是确保网络通信安全的重要手段&#xff0c;它通过加密技术保护传输的数据不被窃听和篡改。在服务器上配置TLS证书&#xff0c;可以使用户通过HTTPS协议安全地访问您的网站。本文将详细介绍如何在服务器上生成一个TLS证…...

k8s业务程序联调工具-KtConnect

概述 原理 工具作用是建立了一个从本地到集群的单向VPN&#xff0c;根据VPN原理&#xff0c;打通两个内网必然需要借助一个公共中继节点&#xff0c;ktconnect工具巧妙的利用k8s原生的portforward能力&#xff0c;简化了建立连接的过程&#xff0c;apiserver间接起到了中继节…...

C++八股 —— 单例模式

文章目录 1. 基本概念2. 设计要点3. 实现方式4. 详解懒汉模式 1. 基本概念 线程安全&#xff08;Thread Safety&#xff09; 线程安全是指在多线程环境下&#xff0c;某个函数、类或代码片段能够被多个线程同时调用时&#xff0c;仍能保证数据的一致性和逻辑的正确性&#xf…...

ABAP设计模式之---“简单设计原则(Simple Design)”

“Simple Design”&#xff08;简单设计&#xff09;是软件开发中的一个重要理念&#xff0c;倡导以最简单的方式实现软件功能&#xff0c;以确保代码清晰易懂、易维护&#xff0c;并在项目需求变化时能够快速适应。 其核心目标是避免复杂和过度设计&#xff0c;遵循“让事情保…...

JVM 内存结构 详解

内存结构 运行时数据区&#xff1a; Java虚拟机在运行Java程序过程中管理的内存区域。 程序计数器&#xff1a; ​ 线程私有&#xff0c;程序控制流的指示器&#xff0c;分支、循环、跳转、异常处理、线程恢复等基础功能都依赖这个计数器完成。 ​ 每个线程都有一个程序计数…...

计算机基础知识解析:从应用到架构的全面拆解

目录 前言 1、 计算机的应用领域&#xff1a;无处不在的数字助手 2、 计算机的进化史&#xff1a;从算盘到量子计算 3、计算机的分类&#xff1a;不止 “台式机和笔记本” 4、计算机的组件&#xff1a;硬件与软件的协同 4.1 硬件&#xff1a;五大核心部件 4.2 软件&#…...

Golang——7、包与接口详解

包与接口详解 1、Golang包详解1.1、Golang中包的定义和介绍1.2、Golang包管理工具go mod1.3、Golang中自定义包1.4、Golang中使用第三包1.5、init函数 2、接口详解2.1、接口的定义2.2、空接口2.3、类型断言2.4、结构体值接收者和指针接收者实现接口的区别2.5、一个结构体实现多…...

解析奥地利 XARION激光超声检测系统:无膜光学麦克风 + 无耦合剂的技术协同优势及多元应用

在工业制造领域&#xff0c;无损检测&#xff08;NDT)的精度与效率直接影响产品质量与生产安全。奥地利 XARION开发的激光超声精密检测系统&#xff0c;以非接触式光学麦克风技术为核心&#xff0c;打破传统检测瓶颈&#xff0c;为半导体、航空航天、汽车制造等行业提供了高灵敏…...

掌握 HTTP 请求:理解 cURL GET 语法

cURL 是一个强大的命令行工具&#xff0c;用于发送 HTTP 请求和与 Web 服务器交互。在 Web 开发和测试中&#xff0c;cURL 经常用于发送 GET 请求来获取服务器资源。本文将详细介绍 cURL GET 请求的语法和使用方法。 一、cURL 基本概念 cURL 是 "Client URL" 的缩写…...

《信号与系统》第 6 章 信号与系统的时域和频域特性

目录 6.0 引言 6.1 傅里叶变换的模和相位表示 6.2 线性时不变系统频率响应的模和相位表示 6.2.1 线性与非线性相位 6.2.2 群时延 6.2.3 对数模和相位图 6.3 理想频率选择性滤波器的时域特性 6.4 非理想滤波器的时域和频域特性讨论 6.5 一阶与二阶连续时间系统 6.5.1 …...