分布式主键生成服务
目录
一、使用线程安全的类——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 二、主键生成最简单写法(不推荐) 三、主键生成方法一:Long型id生成——雪花算法 四、主键生成方法二:流水号 (一)流水号概述 (二)添加配置 1.pom.xml 2.application.properties 3.创…...
【Day50 LeetCode】图论问题 Ⅷ
一、图论问题 Ⅷ 1、dijkstra算法 堆优化 采用堆来优化,适合节点多的稀疏图。代码如下: # include<iostream> # include<vector> # include<list> # include<queue> # include<climits>using namespace std;class myco…...
人大金仓KCA | 用户与角色
人大金仓KCA | 用户与角色 一、知识预备1. 用户和角色 二、具体实施1. 用户管理-命令行1.1 创建和修改用户1.2 修改用户密码1.3 修改用户的并发连接数1.4 修改用户的密码有效期 2.用户管理-EasyKStudio2.1 创建和修改用户2.2 修改用户密码2.3 修改用户的并发连接数2.4 修改用户…...
嵌入式开发:傅里叶变换(4):在 STM32上面实现FFT(基于STM32L071KZT6 HAL库+DSP库)
目录 步骤 1:准备工作 步骤 2:创建 Keil 项目,并配置工程 步骤 3:在MDK工程上添加 CMSIS-DSP 库 步骤 5:编写代码 步骤 6:配置时钟和优化 步骤 7:调试与验证 步骤 8:优化和调…...
【AI学习从零至壹】Numpy基础知识
PyTorch基础知识 Numpy基础NumPy 基本数据类型Numpy数组 NumPy 基础数组创建Numpy特殊数组创建Numpy数组的访问NumPy数组的遍历Numpy数组的常用属性比较常用的属性有: Numpy数组的基本操作Numpy数组的数学操作加减乘除 Numpy线性代数Numpy广播机制 Numpy基础 NumPy…...
Day11,Hot100(贪心算法)
贪心 (1)121. 买卖股票的最佳时机 第 i 天卖出的最大利润,即在前面最低价的时候买入 class Solution:def maxProfit(self, prices: List[int]) -> int:min_price prices[0]ans 0for price in prices:ans max(ans, price - min_price…...
Transformer 代码剖析1 - 数据处理 (pytorch实现)
引言 Transformer 架构自《Attention Is All You Need》论文发表以来,在自然语言处理领域引起了巨大的变革。它摒弃了传统的循环结构,完全基于注意力机制,显著提高了处理序列数据的效率和性能。本文将通过对一个具体的项目代码结构进行详细分…...
Python--模块(下)
3. 内置模块 3.1 os模块 常用功能: os.mkdir("new_dir") # 创建目录 os.listdir(".") # 列出当前目录文件 os.path.join("dir", "file.txt") # 路径拼接 os.path.abspath(__file…...
Android Studio超级详细讲解下载、安装配置教程(建议收藏)
博主介绍:✌专注于前后端、机器学习、人工智能应用领域开发的优质创作者、秉着互联网精神开源贡献精神,答疑解惑、坚持优质作品共享。本人是掘金/腾讯云/阿里云等平台优质作者、擅长前后端项目开发和毕业项目实战,深受全网粉丝喜爱与支持✌有…...
PS画笔工具
画笔工具: 画笔工具(B)(原理:单位笔刷的连续填充,文件格式.abr):圆形矢量笔刷、动态矢量画笔(旧版画笔里有 与压感笔有关)、图案填充画笔 shift画笔ÿ…...
[Java基础] JVM常量池介绍(BeanUtils.copyProperties(source, target)中的属性值引用的是同一个对象吗)
文章目录 1. JVM内存模型2. 常量池中有什么类型?3. 常量池中真正存储的内容是什么4. 判断一个字符串(引用)是否在常量池中5. BeanUtils.copyProperties(source, target)中的属性值引用的是同一个对象吗?6. 获取堆内存使用情况、非堆内存使用情况 1. JVM内…...
1.68M 免安装多格式图片批量转 webp 无广告软件推荐
软件介绍 今天要给大家分享一款超实用的图片处理工具,它能实现多格式图片向 webp 格式的转换,无论是 jpg、png、tif、gif 还是 webp 格式自身的图片,都能批量且借助多线程技术进行转换。 直接打开就能用,体积小巧,仅 …...
LeetCode 1472.设计浏览器历史记录:一个数组完成模拟,单次操作均O(1)
【LetMeFly】1472.设计浏览器历史记录:一个数组完成模拟,单次操作均O(1) 力扣题目链接:https://leetcode.cn/problems/design-browser-history/ 你有一个只支持单个标签页的 浏览器 ,最开始你浏览的网页是 homepage ,…...
[笔记.AI]AI知识科普提纲
仅供参考 1.AI基础认知 1.1什么是什么AI 1.2核心概念 1.2.1机器学习、深度学习、神经网络 1.2.2模型:模型、大模型、模型参数 1.2.3多模态 1.2.4生成式AI & 判别式AI 1.3发展与现状 2.大模型 2.1主流大模型 2.1.1分类 2.1.2各…...
学习知识的心理和方法杂记-01
前言: 1 学习新知识要讲究方法,“知识未学 方法先行”,写本系列文章是为了给自己加深大脑“条件反射”的,因为我自己学习新知识的过程中老会被不科学的“杂念”干扰,导致学习效率低下。 2 关于天才和普通人ÿ…...
网页制作10-html,css,javascript初认识の适用XHTML
一、简介: Xhtml是extensible hypertext markup language的缩写。它是由国际W3C组织制定并公布发行的。是一个过渡技术,结合了部分xml的强大功能及大多数html的简单特性。 Advantage. Xhtml提倡更简洁规范的代码。 Xhtml.文档在旧的基于的浏览器中&…...
C++ 中 cin 和 cout 教程
一、概述 在 C 里,cin 和 cout 是标准库 <iostream> 中用于输入输出操作的重要对象,它们基于流的概念,为开发者提供了方便且类型安全的输入输出方式。cin 是标准输入流对象,主要用于从标准输入设备(一般是键盘&…...
Qt for Android下QMessageBox背景黑色、文字点击闪烁
最近在基于Qt开发安卓应用的时候,在红米平板上默认QMessageBox出现之后,背景黑色,并且点击提示文字会出现闪烁,影响用户体验。 问题分析 1、设置QMessageBox样式,设置背景色、文字颜色,如下所示: QMessageBox {background: white;color: white; } 尝试之后,问题仍存…...
C++20的指定初始化器(Designated Initializers)
文章目录 指定初始化器的使用条件语法嵌套结构体的初始化数组的指定初始化注意事项优势 C20引入了**指定初始化器(Designated Initializers)**这一特性,允许在初始化结构体、联合体或类的对象时,明确指定成员变量的初始化值&#…...
Windows 11【1001问】删除Win11左下角小组件的6种方法
在Windows 11中,左下角的小组件功能虽然提供了天气、新闻等实用信息,但对于一些用户来说可能显得多余或干扰视线。因此,微软提供了多种方式让用户能够自定义是否显示这些小组件。以下是 6 种常见的设置方法来隐藏或关闭Windows 11左下角的小组…...
kotlin的函数标准库使用
摘要说明 函数标准库常用的有: 1、apply: apply函数作为一个配置函数,可以传入一个接收者,然后调用一系列函数来配置它以方便使用,如果提供lambda给apply函数执行,它会返回配置好的接收者 使用介绍&#x…...
深入剖析:自定义实现C语言中的atoi函数
在C语言的标准库中, atoi 函数是一个非常实用的工具,它能够将字符串形式的数字转换为对应的整数。然而,当我们深入探究其实现原理时,会发现其中蕴含着许多有趣的编程技巧和细节。本文将详细讲解如何自定义实现一个类似 atoi 功能的…...
Kubernetes (K8S) 核心原理深度剖析:从架构设计到运行机制
Kubernetes(K8S)作为容器编排领域的“操作系统”,其设计和实现原理是开发者进阶的必修课。本文将从架构设计、核心组件协作、关键机制实现三个维度,结合源码逻辑与实战场景,分享 K8S 的底层运行原理。 一、Kubernetes 架构设计 1. 声明式 API 与控制器模式 K8S 的核心设…...
springboot做接口限流
目录 1. 依赖全局配置2. 注解配置 1. 依赖全局配置 引入依赖 <dependency><groupId>com.github.taptap</groupId><artifactId>ratelimiter-spring-boot-starter</artifactId><version>1.2</version></dependency>appl…...
Visual Studio Code 跨平台安装与配置指南(附官方下载链接)
一、软件定位与核心功能 Visual Studio Code(简称VS Code)是微软开发的开源跨平台代码编辑器,支持超过50种编程语言的智能补全、调试和版本控制功能。2025版本新增AI辅助编程模块,可自动生成单元测试代码和API文档注释。 二、下载…...
TaskBuilder设置排序条件
在整个向导的最后一步,可以设置是否按指定字段的值对查询结果进行排序,支持正序和倒序两种排序方式。如果没有设置任何排序字段,则默认按数据库里现有数据记录的实际存储的先后顺序排序。如果设置了多个排序条件,则按这些条件从上…...
挖src实用脚本开发(二)
文章目录 技术原理代码实现一代码实现二总结 这篇文章记录cms识别脚本。 技术原理 1.使用在线平台识别,比如whatcms,fofa等 2.自己写脚本识别,但是指纹库麻烦,需要耗费大量精力 代码实现一 这里我使用的是whatcms接口࿰…...
[ISP] AE 自动曝光
相机通过不同曝光参数(档位快门时间 x 感光度 x 光圈大小)控制进光量来完成恰当的曝光。 自动曝光流程大概分为三部分: 1. 测光:点测光、中心测光、全局测光等;通过调整曝光档位使sensor曝光在合理的阈值内࿰…...
DeepSeek-R1:通过强化学习激发大语言模型的推理能力
注:此文章内容均节选自充电了么创始人,CEO兼CTO陈敬雷老师的新书《自然语言处理原理与实战》(人工智能科学与技术丛书)【陈敬雷编著】【清华大学出版社】 文章目录 DeepSeek大模型技术系列三DeepSeek大模型技术系列三》DeepSeek-…...
002 docker安装rocketmq
docker search rocketmq#拉取镜像 docker pull foxiswho/rocketmq:server-4.3.2 docker pull foxiswho/rocketmq:broker-4.3.2 #创建nameserver容器 docker create -p 9876:9876 --name rmqserver \ -e "JAVA_OPT_EXT-server -Xms128m -Xmx128m -Xmn128m" \ -e "…...
算法训练(leetcode)二刷第三十七天 | *300. 最长递增子序列、674. 最长连续递增序列、*718. 最长重复子数组
刷题记录 *300. 最长递增子序列674. 最长连续递增序列基础解法(非动规)动态规划 718. 最长重复子数组滚动数组 *300. 最长递增子序列 leetcode题目地址 dp数组含义: dp[i]表示以nums[i]结尾的最长递增子序列长度,即以nums[i]结尾…...
LSTM长短期记忆网络-原理分析
1 简介 概念 LSTM(Long Short-Term Memory)也称为长短期记忆网络,是一种改进的循环神经网络(RNN),专门设计用于解决传统RNN的梯度消失问题和长程依赖问题。LSTM通过引入门机制和细胞状态,能够更…...
Java 面试题 20250227
Java 中序列化与反序列化是什么? 序列化:将 Java 对象转化成可传输的字节序列格式(字节流、JSON、XML),以便于传输和存储。 反序列化:将字节序列格式数据转化成 Java 对象的过程。 1、为什么需要序列化和…...
Spring事务失效六大场景
引言 Spring事务一般我们采用注解实现,但是我们构造事务实现的时候常常没察觉失效的情况,本篇文章总结事务失效的六大情况,帮助我们深刻理解事务失效的边界概念 1. 方法自调用 这个主要是针对声明式事务的,经过前面的介绍&…...
C++和OpenGL实现3D游戏编程【连载23】——几何着色器和法线可视化
欢迎来到zhooyu的C++和OpenGL游戏专栏,专栏连载的所有精彩内容目录详见下边链接: 🔥C++和OpenGL实现3D游戏编程【总览】 1、本节实现的内容 上一节课,我们在Blend软件中导出经纬球模型时,遇到了经纬球法线导致我们在游戏中模型光照显示问题,我们在Blender软件中可以通过…...
Python游戏编程之赛车游戏6-2
3.2 move()方法的定义 Player类的move()方法用于玩家控制汽车左右移动,当玩家点击键盘上的左右按键时,汽车会相应地进行左右移动。 move()方法的代码如图7所示。 图7 move()方法的代码 其中,第20行代码通过pygame.key.get_pressed()函数获…...
Vxe UI 根据vxe-tabs 绑定不同的值,渲染生成不同的 tabls(页签)内容
VxeUI tabs控件,根据绑定不同的内容,动态渲染不同的表格数据放置在不同的 tab 页 效果图如下: 代码实现 <template><vxe-tabs :options"detailTabList"><vxe-tab-pane v-for"(item, index) in detailTabList&…...
Element Plus中el-select选择器的下拉选项列表的样式设置
el-select选择器,默认样式效果: 通过 * { margin: 0; padding: 0; } 去掉内外边距后的样式效果(样式变丑了): 通过 popper-class 自定义类名修改下拉选项列表样式 el-select 标签设置 popper-class"custom-se…...
YOLOv11-ultralytics-8.3.67部分代码阅读笔记-train.py
train.py ultralytics\models\yolo\detect\train.py 目录 train.py 1.所需的库和模块 2.class DetectionTrainer(BaseTrainer): 1.所需的库和模块 # Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/licenseimport math import random from copy…...
PR 安装包 2018-2024(Win,Mac)文中为使用技巧和教程
下载链接:https://pan.baidu.com/s/1LLv1tSXJxUcv6iOlcAHJEg?pwd1234 导语:Adobe Premiere Pro以98%的行业覆盖率和跨平台协作能力,稳居2025年视频剪辑工具榜首。本文涵盖基础配置、核心剪辑、高级调色、效率革命、企业级实战五大模块&…...
请求Geoserver的WTMS服务返回200不返回图片问题-跨域导致
今天碰到个奇怪问题,改了个页面标题再打包布署GeoServer发现调用WTMS服务失败,请求返回状态码200,返回包大小0,使用postman模拟请求是可以正常返回图片的。 跟之前版本对比如下: 正常Response请求: HTTP/1.1 200X-Fr…...
TCP基本入门-简单认识一下什么是TCP
部分内容来源:小林Coding TCP的特点 1.面向连接 一定是“一对一”才能连接,不能像 UDP 协议可以一个主机同时向多个主机发送消息,也就是一对多是无法做到的 2.可靠的 无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个…...
计算机科学技术领域的内卷现状与应对措施分析
计算机科学技术领域的内卷现状与应对措施分析 李升伟 整理 ### 计算机科学技术领域的内卷现状与应对措施分析 #### 一、内卷现状分析 1. **教育与升学内卷** 计算机科学与技术相关专业(如计算机科学与技术、人工智能、大数据等)已成为考研竞争最…...
The First项目报告:VANA如何重塑数据所有权与AI训练
在当今的数字化时代,数据已成为比黄金更为珍贵的资源。科技巨头们通过收集和分析用户的个人数据,获得巨大的商业利益,而用户却往往没有从中得到应有的回报。这种数据的不对等交易和隐私侵犯现象,成为了现代社会的一个严重问题。 …...
pnpm的基本用法
以下是 pnpm 的核心命令和使用指南,涵盖从安装依赖到项目管理的常见操作: 1. 基础命令 (1) 安装依赖 pnpm install # 安装 package.json 中的所有依赖 pnpm install <包名> # 安装指定包(自动添加到 dependencies…...
机试刷题_从上往下打印二叉树【python】
从上往下打印二叉树 # class TreeNode: # def __init__(self, x): # self.val x # self.left None # self.right None # # 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可 # # # param root …...
转化率(漏斗分析)——mysql计算过程
转化率(漏斗分析)——mysql计算过程 问题:有一张表,记录了不同用户的用户id,浏览页面时间,加入购物车时间,下单时间,支付时间,算出每天的各个环节的转化率 创建表info(含用户id,浏…...
《AI和人工智能和编程日报》
OpenAI:将深度研究扩展到 ChatGPT Plus、Team、Edu 和 Enterprise 用户,每月 10 次查询;Pro 用户每月有 120 次查询,ChatGPT 语音模式向免费用户开放。DeepSeek:R1 大模型宣布降价,调用价格将至四分之一&am…...
自然语言处理:稀疏向量表示
介绍 大家好,我是博主。今天又来和大家分享自然语言处理领域的知识了。原本我计划这次分享NLP中文本表示的相关内容,不过在整理分享计划的过程中,发现这部分知识里包含一些涉及复杂数学原理和抽象概念的内容。对于刚接触NLP的小伙伴们来说&a…...
矩阵 trick 系列 题解
1.AT_dp_r Walk(矩阵图论) 题意 一个有向图有 n n n 个节点,编号 1 1 1 至 n n n。 给出一个二维数组 A 1... n , 1... n A_{1...n,1...n} A1...n,1...n,若 A i , j 1 A_{i,j}1 Ai,j1 说明节点 i i i 到节点 j j j …...