请教一个关于并发控制的问题

2025 年 4 月 23 日
 tuoov

现在有这样一个函数 processBatch ,负责读取数据,执行一些操作后再更新它们,相关的数据库操作都在事务内执行。伪代码如下:

function processBatch():
    tx = db.beginTransaction()
    // 1. 批量读取:取出最多 N 条“待处理”数据
    items = tx.query("SELECT * FROM tasks WHERE status = 'PENDING' LIMIT N")
    
    for item in items:
        // 2. 业务处理
        doBusinessLogic(item)
        // 3. 更新状态
        tx.execute("UPDATE tasks SET status = 'DONE' WHERE id = ?", item.id)
    tx.commit()
// 线程 A
spawn threadA:
    processBatch()

// 线程 B (几乎同时执行)
spawn threadB:
    processBatch()

但由于 processBatch 在多个地方都会被调用,因此存在并发问题。线程 A 和线程 B 执行时可能查询到同一批数据,导致这批数据被处理两次。解决这个问题有两个方案:

我的问题是:

  1. 哪个方案更符合最佳实践?原因是什么
  2. 在保持 processBatch 会被多个地方调用不变的前提下,有没有更好的方案?
  3. 如果想学习这类并发相关的问题和解决方案,应该搜索什么关键词

感谢各位赐教

4578 次点击
所在节点    数据库
34 条回复
netnr
2025 年 4 月 23 日
这类似发短信的系统,很多地方调用发短信接口,都先写入发送记录表,状态为待发送,然后起一个任务循环执行发送并改状态;

如果是在一个进程的前提下,可以用线程安全的先进先出队列,把 processBatch 添加到队列,另起一个线程来消费队列
netnr
2025 年 4 月 23 日
贴一个 C# 实现的类

ThreeK
2025 年 4 月 23 日
1 、要是不能控制调用方并发就推介方案 B ,方案 A 容易等不到锁。
2 、2.1 doBusinessLogic 如果 IO 多计算少可以考虑并发执行。2.2 processBatch 可以写成幂等的,最终一致就行,多执行几遍还能申请加资源。
3 、你这不算高并发,高并发一般不存在同一数据并发处理。高并发并发大但调用都带着唯一 id ,直接分布式锁解决同一 id 并发问题,你这种同一数据多处调用应该是锁等待/唤醒问题。
我认为你们这业务槽点太多
1) 事务太大不能拆就只能等报错了。
2 )改 status 像事件驱动(消息通知),又不知你们写的什么。
3 ) limit N 像是要批处理又好多处调用,调用的地方还不加条件,光用 limit N 来确定数据属于一个事务。。。。
geebos
2025 年 4 月 23 日
这种场景一般用生产-消费者模型,一个线程查,多个线程处理
thevita
2025 年 4 月 24 日
没说清楚啊,与你的事务会会发生冲突的都有啥啊,仅仅同一个逻辑的不同任务吗?有没有 读-写冲突?有没有其他不同粒度、不同逻辑的写-写冲突,doBusinessLogic 里面有不有 外部一致性要求?

超大事务呗,某些系统很常见,并不是所有业务都是互联网,上面的不要看到这种就报警

锁放外部(方案 A )正如你所说,只解决了 processBatch 的并发问题,但是不能避免其他事物的更新,依然可能导致 write-skew ,除非你保证只要该这个表,都拿锁,那和表锁其实也没太大差别,就看你们的数据库实现得整么样了

锁表(方案 B )通过合理的加锁,能避免 write-skew, 但是冲突域会变大,影响系统吞吐,甚至某些 db 可能会阻塞读,但是话又说回来,如果你的场景类似,半夜批量计算,冲突可能低那种,耶完全可以接受

其他方案:
其实具体看你能接受 哪部分 可以被适当取舍,比如上面只讨论了锁的情况,取舍的就是与其他事物的冲突

如果你能接受适当若化 这个超大事务的原子性的话还可以: processBatch 内加锁,这个锁止解决 不同 processBatch 任务间的冲突(更好的办法可能是引入一个协调者来保证 不同 processBatch 尽量不冲突),然后更新使用乐观锁+重试,让 这个 batch 实现最终一致,也不失为一种办法(当然,这里没讨论你的 doBusinessLogic 有不有外部一致性的情况)
prosgtsr
2025 年 4 月 24 日
如果是我的话,我会改成一个线程查,然后多线程领取任务再处理。
要问怎么学,我也不知道,我也是草台班子
listenerri
2025 年 4 月 24 日
问题在哪里发生的,就尽可能在那里解决
NoDataNoBB
2025 年 4 月 24 日
select for update
shangfabao
2025 年 4 月 24 日
上边写的是对的,先 update,毕竟看你的逻辑,是否 update 是没有看执行逻辑的返回结果的
kai1412
2025 年 4 月 24 日
多线程分页取 分配好每页的数量 线程取完各自根据主键 id 更新也不会有冲突
kai1412
2025 年 4 月 24 日
@kai1412 分页查的时候记得根据主键 id 排序
heiya
2025 年 4 月 24 日
实现上来说 A 和 B 都行,不过锁粒度都很大。根据你对 processBatch()发生并发可能性的预测,还有一些别的方案:1.并发频繁发生,可以用#28 的 select for update 2.并发偶尔发生,在表里边加一列版本号,每次更新时对比这个值是否和取出时的值一致,不一致就是有并发。这样的锁粒度控制在行上。不过假如 10 条数据 2 条有并发问题,这种情况又得额外处理。
chiaoyuja
2025 年 4 月 24 日
应用层加锁
加锁可以做到显式控制并发,你能清楚知道「谁在处理」;
不依赖数据库细节,业务逻辑统一,不受数据库种类、配置影响;
代码层面的锁(如 Redis 分布式锁)支持跨进程、跨服务的同步;
开发和调试更直观,出问题也容易定位。
数据库加锁
• 数据库事务的隔离级别越高,性能越差(如 SERIALIZABLE 会阻塞读取);
• 很难用 SQL 实现「原子地读取并更新任务状态」这种逻辑,语法不友好;
• 不同数据库实现不同,如 PostgreSQL 支持 SELECT ... FOR UPDATE SKIP LOCKED ,但 MySQL 实现不同,兼容性差;
• 数据库锁只在事务内有效,不能用于跨服务同步控制;
• 如果多个服务部署在不同机器上,仅靠 DB 锁,还是可能出现争抢/重入的问题。
testliyu
2025 年 4 月 25 日
我们之前是直接加分布式锁

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1127560

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX