Java North

Java 's Blog


  • 首页

  • 分类

  • 作者

  • 归档

  • 关于

秒杀

发表于 2025-12-02

如何设计出健壮的秒杀系统?

在电商大促、新品首发、限量抢购等场景中,秒杀系统是技术架构中最具挑战性的模块之一。它需要在极短时间内应对海量用户请求,同时保证系统的稳定性、数据的一致性以及用户体验的流畅性。

一、秒杀系统的本质与核心挑战

秒杀的本质是:短时间内集中爆发的超高并发写操作,最终落到数据库的更新操作上。其典型特征包括:

  • 时间短:通常只有几分钟甚至几秒钟。
  • 并发高:瞬时QPS可达数十万甚至百万级。
  • 流量不均:99%的请求可能是无效或重复的。
  • 资源有限:库存数量极少(如100件),极易发生超卖。

核心挑战有哪些?

挑战 说明
🔴 超卖问题 库存被扣成负数,导致商品多卖,损害公司利益
🟠 高并发冲击 大量请求瞬间涌入,压垮服务器、缓存、数据库
🟡 缓存击穿 热点Key失效,请求直接打到DB
🔵 接口刷单 黑产使用脚本自动化抢购,破坏公平性
🟣 秒杀URL泄露 提前获取接口地址,绕过前端限制提前抢购
⚪ 服务雪崩 秒杀拖垮整个系统,影响其他正常业务

二、健壮秒杀系统的设计原则

设计秒杀系统的核心思想是:尽可能把流量挡在离数据库最远的地方,通过层层过滤和削峰填谷,让最终到达数据库的请求是可控且有效的。

我们遵循以下五大设计原则:

  1. 前置化处理:尽量在前端或边缘节点完成校验和拦截。
  2. 动静分离:静态资源独立部署,减少后端压力。
  3. 异步化流程:非关键路径异步执行,提升响应速度。
  4. 限流降级:保护系统不被压垮,牺牲部分功能保核心可用。
  5. 数据隔离:秒杀业务独立部署,避免影响主站。

三、秒杀系统架构设计(现代版)

  					+------------------+
                    |   CDN / Edge     | ← 静态页面、JS、图片加速
                    +------------------+
                              ↓
                    +------------------+  
                    |     Nginx        | ← 反向代理、负载均衡、IP限流
                    +------------------+
                              ↓
                    +------------------+  
                    |   API Gateway    | ← 统一入口、鉴权、限流、熔断
                    +------------------+
                              ↓
         +------------------------------------------+
         |               Service Layer              |
         | +----------------+  +----------------+   |
         | | 限流服务       |  | 验证码服务       |   |
         | | (Token Bucket) |  | (滑块/短信)     |   |
         | +----------------+  +----------------+   |
         | +----------------+  +----------------+   |
         | | Redis预减库存   |  | 下单队列        |   |
         | | (Lua脚本原子操作)|  | (RabbitMQ/Kafka)|   |
         | +----------------+  +----------------+   |
         +------------------------------------------+
                              ↓
                    +------------------+
                    |    MySQL         | ← 分库分表 + 乐观锁
                    +------------------+

四、关键技术方案详解

1️⃣ 防止超卖:Redis + 乐观锁 + 预减库存

目标:确保库存不超卖,且高性能

  • 预减库存:秒杀开始前,将商品库存预热到 Redis 中,例如 SET goods:1001:stock 100

  • 原子扣减:使用 Lua 脚本保证“判断库存 + 扣减”原子性:

  • 1
    2
    3
    4
    5
    local stock = redis.call('GET', KEYS[1])
    if not stock then return -1 end
    if tonumber(stock) <= 0 then return 0 end
    redis.call('DECR', KEYS[1])
    return 1
    

数据库最终扣减:异步消费队列时,再用 乐观锁 更新数据库:

1
2
UPDATE goods SET stock = stock - 1, version = version + 1 
WHERE id = ? AND stock > 0 AND version = ?

2️⃣ 防止URL泄露:动态化秒杀链接

目标:防止用户通过F12查看Network提前发起请求

  • 秒杀开始前不暴露真实接口地址。

  • 用户进入页面 → 请求获取“秒杀令牌” → 后台返回加密URL(如 /seckill/act_xxx)。

  • URL可带时效性(如5秒过期),MD5或JWT签名防篡改。

  • 1
    2
    GET /api/seckill/token?goodsId=1001
    → 返回 { "url": "/seckill/execute/abc123", "expire": 5 }
    

3️⃣ 动静分离 & 页面静态化

目标:减少后端计算压力

  • 商品详情页、倒计时、按钮状态等生成静态HTML(如用 FreeMarker、Vue SSR)。
  • 前端通过 AJAX 获取实时数据(如剩余库存、是否已抢完)。
  • 静态资源托管在 CDN,全球加速。

📌 好处:90%的流量被CDN承接,不经过应用服务器。

4️⃣ 接口限流:多层级防护

目标:拦截无效请求,防止系统崩溃

(1)前端限流

  • 按钮点击后禁用5秒,防止连点。
  • 前端增加随机延迟,打散请求洪峰。

(2)Nginx 层限流

1
2
3
4
limit_req_zone $binary_remote_addr zone=seckill:10m rate=10r/s;
location /seckill {
    limit_req zone=seckill burst=20 nodelay;
}

(3)网关层限流(推荐)

  • 使用 令牌桶算法(Token Bucket) 或 漏桶算法(Leaky Bucket)
  • 工具推荐:Guava RateLimiter、Sentinel、Redis + Lua
1
2
3
4
5
6
7
// Guava 示例
RateLimiter limiter = RateLimiter.create(1000); // 每秒1000个令牌
if (limiter.tryAcquire(500, TimeUnit.MILLISECONDS)) {
    // 处理请求
} else {
    // 限流,返回“请求过于频繁”
}

(4)用户级限流

  • 按 userId 或 deviceId 限制单位时间内的请求次数(Redis计数器)。

5️⃣ 防刷机制:人机识别 + 行为分析

目标:识别并拦截黄牛和脚本

  • 验证码:滑块、点选、短信验证码(秒杀前触发)
  • 行为分析:监测请求频率、鼠标轨迹、页面停留时间
  • 设备指纹:识别同一设备多账号刷单
  • 黑名单机制:对恶意IP/设备进行封禁

📌 建议:验证码在“提交订单”环节触发,而非一开始就弹出,提升用户体验。

6️⃣ 异步下单 & 削峰填谷

目标:平滑流量峰值,提升系统吞吐量

  • 成功通过预减库存的请求,写入消息队列(如 Kafka、RabbitMQ)。
  • 后台消费者异步处理下单逻辑(创建订单、扣数据库库存、发短信通知)。
  • 用户立即返回“正在处理”,前端轮询结果或WebSocket推送。

📌 好处:

  • 将瞬时高峰转化为平缓的后台任务流。
  • 即使下游系统短暂不可用,消息队列也能缓冲压力。

7️⃣ 服务降级与熔断

目标:系统异常时仍能提供基本服务

  • 使用 Hystrix 或 Sentinel 实现熔断降级。
  • 当下单服务异常时,自动切换到“排队中”页面或提示“稍后再试”。
  • 关键服务独立部署,避免连锁故障。

📌 示例降级策略:

  • Redis宕机 → 返回“活动太火爆,请稍后再试”
  • DB压力大 → 暂停新订单,继续处理队列中已有请求

8️⃣ 数据库优化:分库分表 + 读写分离

目标:支撑高并发写入

  • 秒杀订单表按 orderId 或 userId 分库分表(如ShardingSphere)。
  • 使用MySQL主从架构,写走主库,读走从库。
  • 冷热分离:历史订单归档,提升查询性能。

五、完整秒杀流程图

六、总结:构建健壮秒杀系统的 Checklist

类别 关键措施
✅ 防超卖 Redis预减 + Lua原子操作 + DB乐观锁
✅ 抗高并发 CDN + Nginx + Redis集群 + 消息队列
✅ 防刷 动态URL + 验证码 + 用户限流 + 设备指纹
✅ 系统稳定 服务降级 + 熔断 + 监控告警
✅ 体验优化 页面静态化 + 异步下单 + WebSocket通知
✅ 数据安全 秒杀独立数据库 + 分库分表 + 备份机制

七、结语

秒杀系统不是简单的“抢购功能”,而是一套涉及前端、网络、缓存、中间件、数据库、安全、监控的综合性高并发架构工程。

设计秒杀系统的关键在于:提前预判风险,层层设防,把复杂留给自己,把简单留给用户。

随着技术发展,越来越多的公司开始采用 Serverless 架构、边缘计算、AI反作弊 等新技术来进一步优化秒杀体验。但万变不离其宗——流量控制、资源隔离、数据一致,依然是我们永恒的主题。

阅读全文 »

左匹配

发表于 2025-11-05
什么是联合索引?

首先,要理解最左匹配原则,得先知道什么是联合索引。

单列索引:只针对一个表列创建的索引。例如,为 users 表的 name 字段创建一个索引。 联合索引:也叫复合索引,是针对多个表列创建的索引。例如,为 users 表的 (last_name, first_name) 两个字段创建一个联合索引。 这个索引的结构可以想象成类似于电话簿或字典。电话簿是先按姓氏排序,在姓氏相同的情况下,再按名字排序。你无法直接跳过姓氏,快速找到一个特定的名字。

什么是最左匹配原则?

最左匹配原则指的是:在使用联合索引进行查询时,MySQL/SQL数据库从索引的最左前列开始,并且不能跳过中间的列,一直向右匹配,直到遇到范围查询(>、<、BETWEEN、LIKE)就会停止匹配。

这个原则决定了你的 SQL 查询语句是否能够使用以及如何高效地使用这个联合索引。

核心要点:

从左到右:索引的使用必须从最左边的列开始。 不能跳过:不能跳过联合索引中的某个列去使用后面的列。 范围查询右停止:如果某一列使用了范围查询,那么它右边的列将无法使用索引进行进一步筛选。 举例说明 假设我们有一个 users 表,并创建了一个联合索引 idx_name_age,包含 (last_name, age) 两个字段。

image-20251104232733383

索引 idx_name_age 在磁盘上大致是这样排序的(先按 last_name 排序,last_name 相同再按 age 排序):

(Li, 30) (Wang, 20) (Wang, 22) (Zhang, 25) (Zhang, 28)

现在,我们来看不同的查询场景:

✅ 场景一:完全匹配最左列
1
SELECT * FROM users WHERE last_name = 'Wang';

分析:查询条件包含了索引的最左列 last_name。 索引使用情况:✅ 可以使用索引。数据库可以快速在索引树中找到所有 last_name = ‘Wang’ 的记录((Wang, 20) 和 (Wang, 22))。

✅ 场景二:匹配所有列
1
SELECT * FROM users WHERE last_name = 'Wang' AND age = 22;

分析:查询条件包含了索引的所有列,并且顺序与索引定义一致。 索引使用情况:✅ 可以高效使用索引。数据库先定位到 last_name = ‘Wang’,然后在这些结果中快速找到 age = 22 的记录。

✅ 场景三:匹配最左连续列
1
SELECT * FROM users WHERE last_name = 'Zhang';

分析:虽然只用了 last_name,但它是索引的最左列。 索引使用情况:✅ 可以使用索引。和场景一类似。

❌ 场景四:跳过最左列
1
SELECT * FROM users WHERE age = 25;

分析:查询条件没有包含索引的最左列 last_name。 索引使用情况:❌ 无法使用索引。这就像让你在电话簿里直接找所有叫“伟”的人,你必须翻遍整个电话簿,也就是全表扫描。

⚠️ 场景五:包含最左列,但中间有断档
1
2
-- 假设我们有一个三个字段的索引 (col1, col2, col3)
-- 查询条件为 WHERE col1 = 'a' AND col3 = 'c';

分析:虽然包含了最左列 col1,但跳过了 col2 直接查询 col3。 索引使用情况:✅ 部分使用索引。数据库只能使用 col1 来缩小范围,找到所有 col1 = ‘a’ 的记录。对于 col3 的过滤,它无法利用索引,需要在第一步的结果集中进行逐行筛选。

⚠️ 场景六:最左列是范围查询
1
SELECT * FROM users WHERE last_name > 'Li' AND age = 25;

分析:最左列 last_name 使用了范围查询 >。 索引使用情况:✅ 部分使用索引。数据库可以使用索引找到所有 last_name > ‘Li’ 的记录(即从 Wang 开始往后的所有记录)。但是,对于 age = 25 这个条件,由于 last_name 已经是范围匹配,age 列在索引中是无序的,因此数据库无法再利用索引对 age 进行快速筛选,只能在 last_name > ‘Li’ 的结果集中逐行检查 age。

总结与最佳实践

最左匹配原则的本质是由索引的数据结构(B+Tree) 决定的。索引按照定义的字段顺序构建,所以必须从最左边开始才能利用其有序性。

如何设计好的联合索引?

  • 高频查询优先:将最常用于 WHERE 子句的列放在最左边。
  • 等值查询优先:将经常进行等值查询(=)的列放在范围查询(>, <, LIKE)的列左边。
  • 覆盖索引:如果查询的所有字段都包含在索引中(即覆盖索引),即使不符合最左前缀,数据库也可能直接扫描索引来避免回表,但这通常发生在二级索引扫描中,效率依然不如最左匹配。
阅读全文 »

Java导出复杂excel

发表于 2025-11-05

解析复杂表格

在实际开发中,上传excel文件是十分常见的问题,一般情况下,解析的思路无非1. 固定表头进行解析;2. 每一行进行解析。但是偶尔会碰一下一些格式比较复杂的表格,用以上方式解析就 得不到我们想要的结果了。 例如以下这张表,乍一看是不是有种心态崩溃的感觉,

在这里插入图片描述

面对这种复杂表格,就需要采取特殊的方式了,首先,还是将思路,实现放到最后再说;1.按照每一行去解析,但是在解析时,需要判断是否为单元格;2. 得到数据后,还需要根据行号进行过滤,然后对每一行单元格数据进行合并操作;3. 得到数据后,最后需要根据列号进行过滤,对每一列单元格进行合并操作。

实现 话不多说,上代码:

判断是否为单元格:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
	 *
	 *
	 * @param sheet
	 *            表单
	 * @param cellRow
	 *            被判断的单元格的行号
	 * @param cellCol
	 *            被判断的单元格的列号
	 * @return row: 行数;col列数
	 * @throws IOException
	 * @Author zhangxinmin
	 */
	private static Map<String, Integer> getMergerCellRegionRow(Sheet sheet, int cellRow,
											  int cellCol) {
		Map<String, Integer> map = new HashMap<>();
		int retVal = 0, retCol= 0 ;
		int sheetMergerCount = sheet.getNumMergedRegions();
		for (int i = 0; i < sheetMergerCount; i++) {
			CellRangeAddress cra = (CellRangeAddress) sheet.getMergedRegion(i);
			int firstRow = cra.getFirstRow(); // 合并单元格CELL起始行
			int firstCol = cra.getFirstColumn(); // 合并单元格CELL起始列
			int lastRow = cra.getLastRow(); // 合并单元格CELL结束行
			int lastCol = cra.getLastColumn(); // 合并单元格CELL结束列
			if (cellRow >= firstRow && cellRow <= lastRow) { // 判断该单元格是否是在合并单元格中
				if (cellCol >= firstCol && cellCol <= lastCol) {
					retVal = lastRow - firstRow + 1; // 得到合并的行数
					retCol = lastCol - firstCol + 1; // 得到合并的列数
					break;
				}
			}
		}
		map.put("row", retVal);
		map.put("col", retCol);
		return map;
	}

private static Integer isMergedRegion(Sheet sheet,int row ,int column) {
		int sheetMergeCount = sheet.getNumMergedRegions();
		for (int i = 0; i < sheetMergeCount; i++) {
			CellRangeAddress range = sheet.getMergedRegion(i);
			int firstColumn = range.getFirstColumn();
			int lastColumn = range.getLastColumn();
			int firstRow = range.getFirstRow();
			int lastRow = range.getLastRow();
			if(row >= firstRow && row <= lastRow){
				if(column >= firstColumn && column <= lastColumn){
					return i;
				}
			}
		}
		return -1;
	}

解析代码: CellRowAndColDTO定义类:

1
2
3
4
5
6
7
8
9
10
@Getter
@Setter
public class CellRowAndColDTO {
    private String cellValue;
    private Integer row;
    private Integer col;
    private Integer cellRow;
    private Integer cellCol;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
public static List<CellRowAndColDTO> readDiffDataBySheet(Sheet sheet, int startRows){
		List<CellRowAndColDTO> result = new ArrayList<>();
		for (int rowIndex = startRows, z = sheet.getLastRowNum(); rowIndex <= z; rowIndex++) {
			Row row = sheet.getRow(rowIndex);
			if (row == null) {
				continue;
			}

			int rowSize = row.getLastCellNum();
			for (int columnIndex = 0; columnIndex < rowSize; columnIndex++) {
				CellRowAndColDTO dto = new CellRowAndColDTO();
				Cell cell = row.getCell(columnIndex);
				if (cell != null){
					// 读取单元格数据格式(标记为字符串)
					cell.setCellType(CellType.STRING);
					String value = cell.getStringCellValue();
					if(0 != isMergedRegion(sheet, rowIndex,columnIndex)){//判断是否合并格
						// 处理有值的cell
//						if (StringUtils.isEmpty(value)) {
//							continue;
//						}
						dto.setRow(rowIndex);
						dto.setCol(columnIndex);
						Map<String, Integer> map = getMergerCellRegionRow(sheet, rowIndex, columnIndex);//获取合并的行列
						dto.setCellCol(map.get("col") == 0? 1:map.get("col"));
						dto.setCellRow(map.get("row") == 0? 1:map.get("row"));
						dto.setCellValue(value);
						result.add(dto);

					}else{
						dto.setRow(rowIndex);
						dto.setCol(columnIndex);
						Map<String, Integer> map = getMergerCellRegionRow(sheet, rowIndex, columnIndex);//获取合并的行列
						dto.setCellCol(1);
						dto.setCellRow(1);
						dto.setCellValue(value);
						result.add(dto);
					}

				}
			}
		}
		List<CellRowAndColDTO> dtos = new ArrayList<>();
		Map<Integer, List<CellRowAndColDTO>> map = result.stream().collect(Collectors.groupingBy(CellRowAndColDTO::getRow));//根据行进行分组
		map.forEach((k,v) -> {
			for(int i =0;i<v.size();i++){
				if(i!=0){
					Integer col = dtos.get(dtos.size()-1).getCol()+dtos.get(dtos.size()-1).getCellCol();
					if(v.get(i).getCol() == col){
						dtos.add(v.get(i));
						continue;
					}
				}else{
					dtos.add(v.get(i));
				}

			}
		});

		List<CellRowAndColDTO> dtos2 = new ArrayList<>();
		Map<Integer, List<CellRowAndColDTO>> map2 = dtos.stream().collect(Collectors.groupingBy(CellRowAndColDTO::getCol));//根据列分组
		map2.forEach((k,v) -> {
			for(int i =0;i<v.size();i++){
				if(i!=0){
					if(v.get(i).getCellRow() != 1){
						if(v.get(i).getCellCol() == v.get(i-1).getCellCol() && v.get(i).getCellRow() == v.get(i-1).getCellRow()){
							if(v.get(i).getCellRow() == 1 && v.get(i).getCellCol() == 1){
								dtos2.add(v.get(i));
								continue;
							}else{
								if(StringUtils.isBlank((v.get(i).getCellValue()))){
									continue;
								}
							}
						}
					}

				}
				dtos2.add(v.get(i));
			}
		});
		return dtos2;
	}

说明一下: 首先我先获取每一行,然后对该行的每一个单元格cell对象进行判断处理,判断时候为单元格,如果是,则将行号,列号,合并行数,合并列数,数值进行存储,存储到List集合;然后,对该集合进行过滤操作,通过java8 stream流的方式先根据行号进行分组,然后获取下一个格的位置col ,然后进行判断,如果是下一个格则进行存储;如果是该单元格内的空格,则跳出循环。然后再根据列进行分组,根据行号列号进行对合并格其他空格单元格进行过滤,最后完成数据库存储,完成解析操作。

普通表格按行解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
public static Workbook readWorkBook(String fileType, InputStream is) throws IOException {
	if ("xls".equals(fileType)) {
		return new HSSFWorkbook(is);
	} else if ("xlsx".equals(fileType)) {
		return new XSSFWorkbook(is);
	} else {
		throw new IllegalArgumentException("不支持的文件类型,仅支持xls和xlsx");
	}
}

public static List<String[]> readData(String fileType, int startRows, boolean ignoreRowBlank, InputStream is) throws IOException {
		List<String[]> result = new ArrayList<>();

		Workbook wb = readWorkBook(fileType, is);
		for (int sheetIndex = 0; sheetIndex < wb.getNumberOfSheets(); sheetIndex++) {
			Sheet sheet = wb.getSheetAt(sheetIndex);

			for (int rowIndex = startRows, z = sheet.getLastRowNum(); rowIndex <= z; rowIndex++) {
				Row row = sheet.getRow(rowIndex);
				if (row == null) {
					continue;
				}

				int rowSize = row.getLastCellNum();
				String[] values = new String[rowSize];
				boolean hasValue = false;
				for (int columnIndex = 0; columnIndex < rowSize; columnIndex++) {
					String value = "";
					Cell cell = row.getCell(columnIndex);
					if (cell != null) {
						// 注意:一定要设成这个,否则可能会出现乱码,后面版本默认设置
						switch (cell.getCellType()) {
							case HSSFCell.CELL_TYPE_STRING:
								value = cell.getStringCellValue();
								break;
							case HSSFCell.CELL_TYPE_NUMERIC:
								if (HSSFDateUtil.isCellDateFormatted(cell)) {
									Date date = cell.getDateCellValue();
									if (date != null) {
										value = new SimpleDateFormat("yyyy-MM-dd")
												.format(date);
									} else {
										value = "";
									}
								} else {
									//value = new DecimalFormat("0").format(cell.getNumericCellValue());
									if (HSSFDateUtil.isCellDateFormatted(cell)) {
										value = String.valueOf(cell.getDateCellValue());
									} else {
										cell.setCellType(Cell.CELL_TYPE_STRING);
										String temp = cell.getStringCellValue();
										// 判断是否包含小数点,如果不含小数点,则以字符串读取,如果含小数点,则转换为Double类型的字符串
										if (temp.indexOf(".") > -1) {
											value = String.valueOf(new Double(temp)).trim();
										} else {
											value = temp.trim();
										}
									}
								}
								break;
							case HSSFCell.CELL_TYPE_FORMULA:
								// 导入时如果为公式生成的数据则无值
								if (!cell.getStringCellValue().equals("")) {
									value = cell.getStringCellValue();
								} else {
									value = cell.getNumericCellValue() + "";
								}
								break;
							case HSSFCell.CELL_TYPE_BLANK:
								break;
							case HSSFCell.CELL_TYPE_ERROR:
								value = "";
								break;
							case HSSFCell.CELL_TYPE_BOOLEAN:
								value = (cell.getBooleanCellValue() == true ? "Y"

										: "N");
								break;
							default:
								value = "";
						}
					}
					values[columnIndex] = value;
					if (!value.isEmpty()) {
						hasValue = true;
					}
				}
				if (!ignoreRowBlank || hasValue) {//不为忽略空行模式或不为空行
					result.add(values);
				}
			}
		}
		return result;
	}

这里我就不过多叙述这个按行解析了,代码思路比较简单一看就能懂。

org.apache.poi poi-ooxml 3.17

总结 该文章为我总结平时开发过程中解决的难题的经验和思路;如果有更好的解决办法希望能不吝赐教。大家携手在开发的道路上越走越远。不喜勿喷。

原文链接:https://blog.csdn.net/weixin_42803027/article/details/110189928

阅读全文 »

500ai Agent

发表于 2025-11-04

哈喽,大家好,我是了不起。

国内很多软件都使用Mybatis-plus,同时也会使用到其生成ID的算法。

最近遇到了同一服务在并发的情况下不同实例自动生成的id相同的问题。

阅读全文 »

mybatis-plus自动生成主键ID重复原因

发表于 2025-06-10 | 分类于 Java

哈喽,大家好,我是了不起。

国内很多软件都使用Mybatis-plus,同时也会使用到其生成ID的算法。

最近遇到了同一服务在并发的情况下不同实例自动生成的id相同的问题。

阅读全文 »

重磅推荐!19.4k Star 的「自建服务器神器库」,私有化部署爱好者的终极宝藏清单!

发表于 2025-06-10 | 分类于 Java

哈喽,大家好,我是了不起。

国内很多软件都使用Mybatis-plus,同时也会使用到其生成ID的算法。

最近遇到了同一服务在并发的情况下不同实例自动生成的id相同的问题。

阅读全文 »

C919

发表于 2025-05-11 | 分类于 Java

哈喽,大家好,我是了不起。

全球生产大飞机的厂商比较出名的是空客和波音,那么除了我们的中国商飞以外,还有那些商用飞机制造厂商?中国商飞未来发展如何?

阅读全文 »

C919

发表于 2025-04-04 | 分类于 Java

哈喽,大家好,我是了不起。

全球生产大飞机的厂商比较出名的是空客和波音,那么除了我们的中国商飞以外,还有那些商用飞机制造厂商?中国商飞未来发展如何?

阅读全文 »

从手术台上思考

发表于 2025-03-31 | 分类于 Java

哈喽,大家好,我是了不起。

阅读全文 »

从《黑客帝国》《西部世界》到如今的AI

发表于 2025-03-05 | 分类于 Java

哈喽,大家好,我是了不起。

阅读全文 »

使用SseEmitter实现服务端向客户端推送消息

发表于 2025-02-05 | 分类于 Java

哈喽,大家好,我是了不起。

阅读全文 »

使用SseEmitter实现服务端向客户端推送消息

发表于 2024-12-03 | 分类于 Java

哈喽,大家好,我是了不起。

Spring框架中可以使用SseEmitter实现服务端向客户端推送消息,今天盘一盘。

阅读全文 »

猴子都会内网穿透了!

发表于 2024-11-22 | 分类于 Java

哈喽,大家好,我是了不起。

最近把旧电脑搞成了NAS,就像折腾着搞一下内网穿透。看了好几个教程,发现都不是最新版本,根本无法使用。本次给大家分享猴子都能做到的内网穿透步骤。

阅读全文 »

知名开源项目官宣停更,太可惜了!-20240425

发表于 2024-11-21 | 分类于 Java

哈喽,大家好,我是了不起。

最近看到一个知名开源项目官宣停更,还是挺震惊的。

阅读全文 »

SseEmitter

发表于 2024-11-12 | 分类于 Java

哈喽,大家好,我是了不起。

最近看到一个知名开源项目官宣停更,还是挺震惊的。

阅读全文 »

几种更新库存的方法

发表于 2024-09-01 | 分类于 Java

哈喽,大家好,我是了不起。

最近项目上有类似库存更新的业务需求,顺便总结了一下库存更新的几种方式。

阅读全文 »

你认为写代码的乐趣在哪里?

发表于 2024-07-30 | 分类于 Java

哈喽,大家好,我是了不起。

最近一直处于项目管理的位置,没有什么时间写代码,有时候也会忽然思考一下,我们写代码的乐趣在哪里,有哪些乐趣呢?

阅读全文 »

你认为写代码的乐趣在哪里?

发表于 2024-07-30 | 分类于 Java

哈喽,大家好,我是了不起。

最近一直处于项目管理的位置,没有什么时间写代码,有时候也会忽然思考一下,我们写代码的乐趣在哪里,有哪些乐趣呢?

阅读全文 »

Java只有中国人在搞了吗?

发表于 2024-07-30 | 分类于 Java

哈喽,大家好,我是了不起。

最近项目团队找人,我面试了很多人,非常喜欢问一个问题,Java线程池为什么先入队列再增加线程数?

阅读全文 »

同事离职了,谁也没在提过他,就像他从没来过-20240717

发表于 2024-07-17 | 分类于 Java

哈喽,大家好,我是了不起。

今天在脉脉上看到一个有意思的话题,相信工作有一些年头的打工人一定深有感触。

阅读全文 »
1 2 … 20
Java Geek Tech

Java Geek Tech

一群热爱 Java 的技术人

388 日志
126 分类
56 作者
RSS
GitHub 知乎
Links
  • 纯洁的微笑
© 2019 - 2025 Java Geek Tech
由 Jekyll 强力驱动
主题 - NexT.Mist