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

136 天前
 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. 如果想学习这类并发相关的问题和解决方案,应该搜索什么关键词

感谢各位赐教

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

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

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

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

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

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

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

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

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

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

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

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

© 2021 V2EX