Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

很多的业务场景我们会用到锁,包括各种炫酷的分布式锁,但是其实很多情况由于db的可靠性是相对比较高的,所以也可以在适当的情况下使用db来作为锁
这里就介绍下比较常用了 select for update 锁

什么是 SELECT FOR UPDATE

SELECT FOR UPDATE 是 MySQL 中一种特殊的查询语句,它在执行查询的同时对选中的行加上排他锁(Exclusive Lock),确保在当前事务结束之前,其他事务无法修改这些数据。这种机制主要用于解决并发环境下的数据一致性问题。

基本语法

1
SELECT column1, column2, ... FROM table_name WHERE condition FOR UPDATE;

工作原理

当执行 SELECT FOR UPDATE 时,MySQL 会:

  1. 加排他锁:对查询结果中的每一行数据加上排他锁(X锁)
  2. 阻塞其他事务:其他事务如果要修改被锁定的行,必须等待当前事务结束
  3. 事务结束释放:当前事务提交(COMMIT)或回滚(ROLLBACK)时,锁会自动释放

使用场景

1. 防止超卖问题

电商系统中最常见的应用场景:

1
2
3
4
5
6
7
8
9
10
11
12
-- 开始事务
START TRANSACTION;

-- 查询并锁定商品库存
SELECT stock FROM products WHERE id = 1001 FOR UPDATE;

-- 假设查询结果显示库存为 10
-- 用户购买 3 件商品
UPDATE products SET stock = stock - 3 WHERE id = 1001;

-- 提交事务
COMMIT;

这里也可以直接使用start; 开启事务,

2. 生成唯一序列号

确保序列号的唯一性:

1
2
3
4
5
6
7
8
9
10
START TRANSACTION;

-- 获取当前最大序列号并锁定
SELECT max_seq FROM sequence_table WHERE table_name = 'orders' FOR UPDATE;

-- 更新序列号
UPDATE sequence_table SET max_seq = max_seq + 1 WHERE table_name = 'orders';

-- 提交事务
COMMIT;

3. 账户余额操作

银行转账等涉及账户余额的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
START TRANSACTION;

-- 锁定转出账户
SELECT balance FROM accounts WHERE account_id = 'A001' FOR UPDATE;

-- 锁定转入账户
SELECT balance FROM accounts WHERE account_id = 'B001' FOR UPDATE;

-- 执行转账操作
UPDATE accounts SET balance = balance - 1000 WHERE account_id = 'A001';
UPDATE accounts SET balance = balance + 1000 WHERE account_id = 'B001';

COMMIT;

锁的范围和类型

1. 行级锁 vs 表级锁

  • InnoDB 存储引擎:使用行级锁,只锁定符合条件的行
  • MyISAM 存储引擎:使用表级锁,锁定整个表

2. 索引对锁范围的影响

1
2
3
4
5
6
7
8
-- 情况1:使用主键或唯一索引(精确锁定)
SELECT * FROM users WHERE id = 100 FOR UPDATE; -- 只锁定 id=100 的行

-- 情况2:使用普通索引(可能锁定多行)
SELECT * FROM users WHERE age = 25 FOR UPDATE; -- 锁定所有 age=25 的行

-- 情况3:无索引条件(锁定所有行)
SELECT * FROM users WHERE name = 'John' FOR UPDATE; -- 可能锁定整个表

死锁问题与预防

死锁产生的原因

1
2
3
4
5
6
7
8
9
-- 事务 A
START TRANSACTION;
SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
SELECT * FROM table2 WHERE id = 2 FOR UPDATE; -- 等待事务B释放

-- 事务 B(同时执行)
START TRANSACTION;
SELECT * FROM table2 WHERE id = 2 FOR UPDATE;
SELECT * FROM table1 WHERE id = 1 FOR UPDATE; -- 等待事务A释放

这样就会造成死锁等待,注意一点死锁不一定是两个事务相互等待,也可以是循环等待,要达到的是竞态等待

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 事务 A
START TRANSACTION;
SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
SELECT * FROM table2 WHERE id = 2 FOR UPDATE; -- 等待事务B释放

-- 事务 B(同时执行)
START TRANSACTION;
SELECT * FROM table2 WHERE id = 2 FOR UPDATE;
SELECT * FROM table1 WHERE id = 3 FOR UPDATE; -- 等待事务C释放

-- 事务 C(同时执行)
START TRANSACTION;
SELECT * FROM table2 WHERE id = 3 FOR UPDATE;
SELECT * FROM table1 WHERE id = 1 FOR UPDATE; -- 等待事务A释放

这样也会造成死锁,所以不要把死锁就单纯理解为两个线程互相等待

预防死锁的策略

  1. 统一加锁顺序:所有事务按相同顺序获取锁
  2. 减少锁持有时间:尽快提交或回滚事务
  3. 使用较低的隔离级别:在业务允许的情况下
  4. 合理设计索引:避免锁定过多不必要的行

性能优化建议

1. 合理使用索引

1
2
3
4
5
-- 优化前:全表扫描,锁定大量行
SELECT * FROM orders WHERE status = 'pending' FOR UPDATE;

-- 优化后:为 status 字段添加索引
ALTER TABLE orders ADD INDEX idx_status (status);

2. 缩小锁定范围

1
2
3
4
5
-- 避免:锁定所有字段
SELECT * FROM products WHERE category = 'electronics' FOR UPDATE;

-- 推荐:只选择必要字段
SELECT id, stock FROM products WHERE category = 'electronics' FOR UPDATE;

3. 合理的事务边界

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 不推荐:事务过长
START TRANSACTION;
SELECT * FROM table1 FOR UPDATE;
-- 大量业务逻辑处理
-- 网络IO操作
UPDATE table1 SET ...;
COMMIT;

-- 推荐:缩短事务时间
-- 先处理业务逻辑
START TRANSACTION;
SELECT * FROM table1 FOR UPDATE;
UPDATE table1 SET ...;
COMMIT;

注意事项和限制

1. 必须在事务中使用

1
2
3
4
5
6
7
8
-- 错误:不在事务中,锁立即释放
SELECT * FROM users WHERE id = 1 FOR UPDATE;

-- 正确:在事务中使用
START TRANSACTION;
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- 其他操作
COMMIT;

2. 自动提交模式的影响

1
2
3
4
5
6
7
-- 检查自动提交状态
SHOW VARIABLES LIKE 'autocommit';

-- 如果开启了自动提交,需要显式开启事务
SET autocommit = 0; -- 关闭自动提交
-- 或者
START TRANSACTION; -- 显式开启事务

3. 锁等待超时

1
2
-- 设置锁等待超时时间(秒)
SET innodb_lock_wait_timeout = 60;

替代方案

1. 乐观锁

1
2
3
4
-- 使用版本号实现乐观锁
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = 1001 AND version = @old_version;

2. 原子性操作

1
2
3
4
-- 直接使用原子性的 UPDATE
UPDATE products
SET stock = stock - 1
WHERE id = 1001 AND stock >= 1;

总结

SELECT FOR UPDATE 是 MySQL 中解决并发问题的重要工具,但使用时需要注意:

  • 合理设计事务边界,避免长时间持有锁
  • 正确使用索引,减少锁定范围
  • 统一加锁顺序,预防死锁
  • 根据业务场景选择合适的并发控制策略

正确使用 SELECT FOR UPDATE 可以有效保证数据一致性,但也要权衡其对系统性能的影响,在实际应用中需要根据具体业务场景做出合理的选择。

程序员中例如我这样的主要是服务端的其实要开发一款app会面临很多问题,相对便捷的可能是做一个网页版的,套用下组件啥的,网页版的要做成app的话之前比较可行的是通过 Electron ,但是也是学习成本比较大的一件事,现在要介绍的这个pakeplus是基于rust tauri开发的一个傻瓜式地把网页转换成app的开源软件
首先它的github地址是 https://github.com/Sjj1024/PakePlus?tab=readme-ov-file

可以在 https://github.com/Sjj1024/PakePlus/releases 下载对应的版本
下载后打开长这样

点击➕号就能开始创建一个app
然后就是填入网页的地址,比如我这边想把我常用的一个crontab工具打包成一个app,
那就填入地址,以及相关的软件描述,还可以控制是否开启调试等

保存后就可以开始点击预览

这样一个简单的网页就打包成了一个mac上的app了,这里可能更多的场景是比如我就写了一个网页版的小工具,想打包成一个app来方便随时打开使用
比如本地的json工具等等,有些是自己定制的更趁手,还是非常不错的,记得之前这个开发者是可能做的命令行版的
这次是更彻底的界面化的比较傻瓜式的打包工具
方便很多不了解开发,命令行的同学进行使用

最近写代码的时候碰到个问题,
比如我们有一个对象列表,对象中有个排序字段,比如时间或者索性就有个序号

1
2
3
4
5
6
7
8
9
10
11
12
public class Demo {

private int order = 0;

public int getOrder() {
return order;
}

public void setOrder(int order) {
this.order = order;
}
}

比如这样,有一个前提,就是这个列表其实是有序的,只是为了防止万一
所以对这个列表进行了排序,就直接使用了ArrayList自带的sort方法,由于是不小心用在了多线程环境
就有出现了一个并发变更报错
也就是这个异常 java.util.ConcurrentModificationException#ConcurrentModificationException()
看下注释

1
2
This exception may be thrown by methods that have detected concurrent modification of an object when such modification is not permissible.
For example, it is not generally permissible for one thread to modify a Collection while another thread is iterating over it. In general, the results of the iteration are undefined under these circumstances. Some Iterator implementations (including those of all the general purpose collection implementations provided by the JRE) may choose to throw this exception if this behavior is detected. Iterators that do this are known as fail-fast iterators, as they fail quickly and cleanly, rather that risking arbitrary, non-deterministic behavior at an undetermined time in the future.

一般情况就是并发环境下进行了变更,
只是这里比较特殊,这个列表没有进行增删,同时是有序的,理论上应该是不会出现变更才对
但是它的确出现了,就得看下这个产生的原因
主要还是来看下这个java.util.ArrayList#sort的实现跟这个异常的抛出原理

1
2
3
4
5
6
7
public void sort(Comparator<? super E> c) {
final int expectedModCount = modCount;
Arrays.sort((E[]) elementData, 0, size, c);
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
modCount++;
}

可以看到在排序前会有个 modCount 的赋值,如果在进行排序后,这个初始的 modCount 跟当前的不等的话就会抛出这个异常
那么什么时候会出现这两者不相等呢
简单推演下这个并发排序下的情况,线程1和线程2同时进入这个方法,拿到了一样的 modCount , 然后进行排序,第一个线程比较快,sort玩之后发现两者相等
就到了 modCount++ , 这时候第二个线程刚排序完,发现 modCount 已经比 expectedModCount 大了
这个时候就出现了这个异常的抛出时机
如果这么理解这个异常的确是应该会出现
而不是我们理解的,如果不变更元素,不调整顺序,就不会触发这个并发变更的异常
还是有很多值得学习的点

鉴于用upic配置r2存储的图床失败后,目前的方式退化了一下,直接使用rclone作为临时替代
但是也才了个小坑,因为rclone在做copy的时候是不管bucket是否存在的,直接会调用 CreateBucket 命令
同时我们的accessKey一般是只有对bucket内部文件的读写权限,所以会导致一个比较尴尬的问题
直接用copy就是报

1
2025/05/11 21:16:04 NOTICE: Failed to copy: failed to prepare upload: operation error S3: CreateBucket, https response error StatusCode: 403, RequestID: , HostID: , api error AccessDenied: Access Denied

这个403的错误,结果谷歌搜索了下
可以通过这个选项进行屏蔽

1
--s3-no-check-bucket

这样我就能用

1
./rclone --s3-no-check-bucket copy ~/Downloads/r2/test_for_rclone.png r2_new:blog

来上传了,只是这里相比uPic麻烦了两步,第一截图后需要对文件进行命名,不然就是qq截图或者微信截图的文件名,
第二上传后需要自己拼图片的链接,当然希望是能够使用uPic的,但目前的问题是uPic似乎对于s3协议的支持不那么好
噢,对了,我用的是直接基于开源代码的自己打包版本,个人感觉还有点不太习惯从免费用成付费软件,主要是它是开源的
打算空了再折腾打包试试,或者自己调试下,实在不行就付费了

在大模型的演进过程中,mcp是个对于使用者非常有用的一个协议或者说工具,Model Context Protocol (MCP) 是一种专为大型语言模型和 AI 系统设计的通信协议框架,它解决了 AI 交互中的一个核心问题:如何有效地管理、传递和控制上下文信息。打个比方,如果把 AI 模型比作一个智能助手,那么 MCP 就是确保这个助手能够”记住”之前的对话、理解当前问题的背景,并按照特定规则进行回应的通信机制。
MCP 的工作原理
基本结构
MCP 的基本结构可以分为三个主要部分:

  1. 上下文容器 (Context Container):存储对话历史、系统指令和用户背景等信息
  2. 控制参数 (Control Parameters):调节模型行为的设置,如温度、最大输出长度等
  3. 消息体 (Message Body):当前需要处理的输入内容
    ┌─────────────────────────────────┐
    │ MCP 请求/响应结构 │

├─────────────────────────────────┤
│ │
│ ┌─────────────────────────┐ │
│ │ 上下文容器 │ │
│ │ ┌─────────────────┐ │ │
│ │ │ 对话历史 │ │ │
│ │ └─────────────────┘ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ 系统指令 │ │ │
│ │ └─────────────────┘ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ 用户背景 │ │ │
│ │ └─────────────────┘ │ │
│ └─────────────────────────┘ │
│ │
│ ┌─────────────────────────┐ │
│ │ 控制参数 │ │
│ └─────────────────────────┘ │
│ │
│ ┌─────────────────────────┐ │
│ │ 消息体 │ │
│ └─────────────────────────┘ │
│ │
└─────────────────────────────────┘

实操

首先基于start.spring.io创建一个springboot应用,需要springboot的版本3.3+和jdk17+
然后添加maven依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server</artifactId>
<version>1.0.0-M8</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>6.1.12</version>
</dependency>

我们就可以实现一个mcp的demo server

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
96
97
98
99
package com.nicksxs.mcp_demo;

import com.jayway.jsonpath.JsonPath;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;

import java.util.List;
import java.util.Map;

@Service
public class WeatherService {

private final RestClient restClient;

public WeatherService() {
this.restClient = RestClient.builder()
.baseUrl("https://api.weather.gov")
.defaultHeader("Accept", "application/geo+json")
.defaultHeader("User-Agent", "WeatherApiClient/1.0 (your@email.com)")
.build();
}

@Tool(description = "Get weather forecast for a specific latitude/longitude")
public String getWeatherForecastByLocation(
double latitude, // Latitude coordinate
double longitude // Longitude coordinate
) {
// 首先获取点位信息
String pointsResponse = restClient.get()
.uri("/points/{lat},{lon}", latitude, longitude)
.retrieve()
.body(String.class);

// 从点位响应中提取预报URL
String forecastUrl = JsonPath.read(pointsResponse, "$.properties.forecast");

// 获取天气预报
String forecast = restClient.get()
.uri(forecastUrl)
.retrieve()
.body(String.class);

// 从预报中提取第一个周期的详细信息
String detailedForecast = JsonPath.read(forecast, "$.properties.periods[0].detailedForecast");
String temperature = JsonPath.read(forecast, "$.properties.periods[0].temperature").toString();
String temperatureUnit = JsonPath.read(forecast, "$.properties.periods[0].temperatureUnit");
String windSpeed = JsonPath.read(forecast, "$.properties.periods[0].windSpeed");
String windDirection = JsonPath.read(forecast, "$.properties.periods[0].windDirection");

// 构建返回信息
return String.format("Temperature: %s°%s\nWind: %s %s\nForecast: %s",
temperature, temperatureUnit, windSpeed, windDirection, detailedForecast);
// Returns detailed forecast including:
// - Temperature and unit
// - Wind speed and direction
// - Detailed forecast description
}

@Tool(description = "Get weather alerts for a US state")
public String getAlerts(@ToolParam(description = "Two-letter US state code (e.g. CA, NY)") String state) {

// 获取指定州的天气警报
String alertsResponse = restClient.get()
.uri("/alerts/active/area/{state}", state)
.retrieve()
.body(String.class);

// 检查是否有警报
List<Map<String, Object>> features = JsonPath.read(alertsResponse, "$.features");
if (features.isEmpty()) {
return "当前没有活动警报。";
}

// 构建警报信息
StringBuilder alertInfo = new StringBuilder();
for (Map<String, Object> feature : features) {
String event = JsonPath.read(feature, "$.properties.event");
String area = JsonPath.read(feature, "$.properties.areaDesc");
String severity = JsonPath.read(feature, "$.properties.severity");
String description = JsonPath.read(feature, "$.properties.description");
String instruction = JsonPath.read(feature, "$.properties.instruction");

alertInfo.append("警报类型: ").append(event).append("\n");
alertInfo.append("影响区域: ").append(area).append("\n");
alertInfo.append("严重程度: ").append(severity).append("\n");
alertInfo.append("描述: ").append(description).append("\n");
if (instruction != null) {
alertInfo.append("安全指示: ").append(instruction).append("\n");
}
alertInfo.append("\n---\n\n");
}

return alertInfo.toString();
}

// ......
}

通过 api.weather.gov 来请求天气服务,给出结果
然后通过一个客户端来访问请求下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void main(String[] args) {
var stdioParams = ServerParameters.builder("java")
.args("-jar", "/Users/shixuesen/Downloads/mcp-demo/target/mcp-demo-0.0.1-SNAPSHOT.jar")
.build();

var stdioTransport = new StdioClientTransport(stdioParams);

var mcpClient = McpClient.sync(stdioTransport).build();

mcpClient.initialize();

// McpSchema.ListToolsResult toolsList = mcpClient.listTools();

McpSchema.CallToolResult weather = mcpClient.callTool(
new McpSchema.CallToolRequest("getWeatherForecastByLocation",
Map.of("latitude", "47.6062", "longitude", "-122.3321")));
System.out.println(weather.content());

McpSchema.CallToolResult alert = mcpClient.callTool(
new McpSchema.CallToolRequest("getAlerts", Map.of("state", "NY")));

mcpClient.closeGracefully();
}

这个就是实现了mcp的简单示例,几个问题,要注意java的多版本管理,我这边主要是用jdk1.8,切成 17需要对应改maven的配置等

0%