Nicksxs's Blog

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

我们在用一些设计模式的时候,特别是在java跟spring生态中,有很多基于接口,会有各种不同的实现,此时如果想让实现了同一个接口的一组处理器(bean)能够按顺序处理,会有比较多种方法,这里简单介绍两种实现方式,
我们先定义一个接口

1
2
3
public interface Processor {
void process();
}

然后有两个实现
第一个实现是

1
2
3
4
5
6
7
public class FirstProcessor implements Processor{

@Override
public void process() {
System.out.println("this is " + this.getClass().getSimpleName());
}
}

第二个实现是

1
2
3
4
5
6
7
public class SecondProcessor implements Processor{

@Override
public void process() {
System.out.println("this is " + this.getClass().getSimpleName());
}
}

主要是为了示意,所以就把类名打了出来
我们通过一个http接口来调用下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
public class OrderController {

@Autowired
private List<Processor> processors;

@RequestMapping(value = "/order", method = RequestMethod.GET)
@ResponseBody
public void test() {
for (Processor processor : processors) {
processor.process();
}
}
}

这里通过autowired注解进行注入,此时我们可以运行起来看下执行顺序
看到控制台输出的确是有序的

1
2
this is FirstProcessor
this is SecondProcessor

这个并不是spring这么智能知道我们的想法,只是刚好按字母顺序F在前
比如我们再加两个处理器
第三个

1
2
3
4
5
6
7
8
@Service
public class ThirdProcessor implements Processor{

@Override
public void process() {
System.out.println("this is " + this.getClass().getSimpleName());
}
}

和第四个

1
2
3
4
5
6
7
@Service
public class FourthProcessor implements Processor{
@Override
public void process() {
System.out.println("this is " + this.getClass().getSimpleName());
}
}

然后我们请求下,就发现是

1
2
3
4
this is FirstProcessor
this is FourthProcessor
this is SecondProcessor
this is ThirdProcessor

没有按照我们想要的顺序执行
那么我们可以怎么做呢

方法一 使用order注解

通过spring的org.springframework.core.annotation.Order 注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.springframework.core.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@Documented
public @interface Order {
int value() default Integer.MAX_VALUE;
}

通过value值来指定顺序,值越小,顺序越靠前
比如我们这

1
2
3
4
5
6
7
8
9
10
11
12
@Service
@Order(value = 1)
public class FirstProcessor implements Processor{
@Service
@Order(value = 2)
public class SecondProcessor implements Processor{
@Service
@Order(value = 3)
public class ThirdProcessor implements Processor{
@Service
@Order(value = 4)
public class FourthProcessor implements Processor{

运行验证下的确是按照我们想要的顺序执行了

1
2
3
4
this is FirstProcessor
this is SecondProcessor
this is ThirdProcessor
this is FourthProcessor

方法二 实现 Ordered 接口

org.springframework.core.Ordered 接口,需要实现 getOrder 方法

1
2
3
4
5
6
public interface Ordered {
int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;
int LOWEST_PRECEDENCE = Integer.MAX_VALUE;

int getOrder();
}

我们就在处理器里实现这个接口方法

1
2
3
4
5
6
7
8
9
10
11
12
public class FirstProcessor implements Processor, Ordered {

@Override
public void process() {
System.out.println("this is " + this.getClass().getSimpleName());
}

@Override
public int getOrder() {
return 1;
}
}

运行下同样可以实现这个效果

1
2
3
4
this is FirstProcessor
this is SecondProcessor
this is ThirdProcessor
this is FourthProcessor

很多的业务场景我们会用到锁,包括各种炫酷的分布式锁,但是其实很多情况由于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协议的支持不那么好
噢,对了,我用的是直接基于开源代码的自己打包版本,个人感觉还有点不太习惯从免费用成付费软件,主要是它是开源的
打算空了再折腾打包试试,或者自己调试下,实在不行就付费了

0%