关于 Django 的乐观锁问题

2020-11-12 15:24:30 +08:00
 IurNusRay
Django 的乐观锁真的有作用吗?我写了个例子,用 jmeter 开启 1000 个线程去请求,结果并不能解决资源竞争的问题,代码如下:
with transaction.atomic():
save_id = transaction.savepoint()
try:
book = BookInfo.objects.get(id=1)
origin_read = book.read
new_read = origin_read + 1
BookInfo.objects.filter(id=book.id, read=origin_read).update(read=new_read)
book.save()
except Exception as e:
print(e)
transaction.savepoint_rollback(save_id)

transaction.savepoint_commit(save_id)

理论上,book 表的 read 资源在 1000 次请求之后应该会从 0 增加到 1000,然而实际上只有 500 左右,当我降低线程数量到 100,也只能到 78,这是为什么呢
1546 次点击
所在节点    Python
13 条回复
nonduality
2020-11-12 16:06:20 +08:00
你在 try 里头加一行输出 log,看执行情况,也许是有些请求 miss 了(用内置开发服务器的话可能性很大)。

此外,你可以用 F 表达式,按说是可以避免 race condition 问题:

```python
try:
book = BookInfo.objects.get(id=1)
BookInfo.objects.filter(id=book.id, read=origin_read).update(read=F('read')+1)
book.save()
except Exception as e:
....

```
nonduality
2020-11-12 16:10:24 +08:00
try 里头改成这两行差不多就行
book = BookInfo.objects.get(id=1)
BookInfo.objects.filter(id=book.pk).update(read=F('read')+1)
使用 F 表达式后,不确定还需不需要使用 transaction,你可以测试下
IurNusRay
2020-11-12 16:14:17 +08:00
@nonduality F 表达式我也试过,也不行
IurNusRay
2020-11-12 17:16:08 +08:00
之前是用 runserver 运行的,可能并发支持不行,刚刚换成 uwsgi 运行,发现结果如下:
1. book.read += 1 这种方式无法解决资源竞争,实测 1000 次请求,只能加到 500 左右
2. book.read = F("read") + 1 这种方式可以解决,实测 1000 次并发请求,分 5 批,最后 read 值加到了 5000
3. book = BookInfo.objects.get(id=1)
origin_read = book.read
BookInfo.objects.filter(id=book.id, read=origin_read).update(read=origin_read + 1)
这种所谓“乐观锁“的方式,实测完全无效,1000 次请求,read 值只能加到 500 左右

综上,使用 F 表达式是最有效的方式,不是很明白这种乐观锁的作用是什么,既没有解决资源竞争,实际运行也没有任何报错
nonduality
2020-11-12 19:03:49 +08:00
@IurNusRay 我刚测试了一下,用 F 表达式进行数据自加,gunicorn 起 1 个进程跑,用 ab -n 1000 -c 100 测试,完全没问题。所以,用 F 表达式对多数情形下是够用的。
nonduality
2020-11-12 19:20:58 +08:00
我不太清楚谁提出来“乐观锁”,但看其实现,大概是要保证 filter 到的实例状态具备 origin_read 的值,在此基础上 update 数值,但就算有 trasaction,也无法保证它一定获取到你要的数据状态,自然就 miss 掉了。但 F 表达式不一样,它用 SQL 语句在数据库层面直接操作数据的。
IurNusRay
2020-11-13 09:25:48 +08:00
@nonduality 刚刚有看了一下,原来是我代码漏掉了一部分,这个"乐观锁"的原理是要开启一个循环,在成功+1 的时候退出循环,否则继续, 比如 row = BookInfo.objects.filter(id=book.id, read=origin_read).update(read=origin_read + 1),当 row 为 0 时继续循环。

但是经过测试,仍然达不到 F 表达式的效果,1000 次请求只能加到 990 左右,所以,还是用 F 表达式吧
todd7zhang
2020-11-13 09:32:28 +08:00
@IurNusRay 我试过,django3.1, 默认使用 sqlite3 数据库,不开启事务的时候。1000 次,20 并发 sleep 重试是能实现的
todd7zhang
2020-11-13 09:33:32 +08:00
todd7zhang
2020-11-13 09:49:54 +08:00
如果在外面包一个 atomic, 在执行 filter().update 时,会触发 sqlite3.OperationalError: database is locked 。然后尝试对 save_point rollback 时又有新的 An error occurred in the current transaction. You can't execute queries until the end of the 'atomic' block 。不知道是不是 sqlite 的 isolation level 导致的,我后面试一下 postgreSQL 。 按理来说,只要是 read committed 就可以的
todd7zhang
2020-11-13 09:58:33 +08:00
实测 postgreSQL 没毛病, 1000 个请求,20 并发。https://paste.ubuntu.com/p/pxdPM9B8Cx/

所有的服务都是直接 runserver
nonduality
2020-11-13 10:07:23 +08:00
@todd7zhang 我用 F 表达式,不用 transaction,1000 请求,100 并发(尝试过更高,但受系统限制开不起来),多次测试都完全正常。如果不涉及关键的数值,用 F 表达式足够了,用 transaction 降低数据库性能。
todd7zhang
2020-11-13 10:42:35 +08:00
@nonduality 这种 filter().update() 主要还是为了防止超售吧,如果你单纯的为了+1, F 表达式确实可以

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

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

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

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

© 2021 V2EX