关于 Django 的乐观锁问题 - V2EX
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
IurNusRay
0.54D

关于 Django 的乐观锁问题

  •  
  •   IurNusRay Nov 12, 2020 1977 views
    This topic created in 2007 days ago, the information mentioned may be changed or developed.
    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,这是为什么呢
    13 replies    2020-11-13 10:42:35 +08:00
    nonduality
        1
    nonduality  
       Nov 12, 2020
    你在 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
        2
    nonduality  
       Nov 12, 2020
    try 里头改成这两行差不多就行
    book = BookInfo.objects.get(id=1)
    BookInfo.objects.filter(id=book.pk).update(read=F('read')+1)
    使用 F 表达式后,不确定还需不需要使用 transaction,你可以测试下
    IurNusRay
        3
    IurNusRay  
    OP
       Nov 12, 2020
    @nonduality F 表达式我也试过,也不行
    IurNusRay
        4
    IurNusRay  
    OP
       Nov 12, 2020
    之前是用 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
        5
    nonduality  
       Nov 12, 2020
    @IurNusRay 我刚测试了一下,用 F 表达式进行数据自加,gunicorn 起 1 个进程跑,用 ab -n 1000 -c 100 测试,完全没问题。所以,用 F 表达式对多数情形下是够用的。
    nonduality
        6
    nonduality  
       Nov 12, 2020
    我不太清楚谁提出来“乐观锁”,但看其实现,大概是要保证 filter 到的实例状态具备 origin_read 的值,在此基础上 update 数值,但就算有 trasaction,也无法保证它一定获取到你要的数据状态,自然就 miss 掉了。但 F 表达式不一样,它用 SQL 语句在数据库层面直接操作数据的。
    IurNusRay
        7
    IurNusRay  
    OP
       Nov 13, 2020
    @nonduality 刚刚有看了一下,原来是我代码漏掉了一部分,这个"乐观锁"的原理是要开启一个循环,在成功+1 的时候退出循环,否则继续, 比如 row = BookInfo.objects.filter(id=book.id, read=origin_read).update(read=origin_read + 1),当 row 为 0 时继续循环。

    但是经过测试,仍然达不到 F 表达式的效果,1000 次请求只能加到 990 左右,所以,还是用 F 表达式吧
    todd7zhang
        8
    todd7zhang  
       Nov 13, 2020
    @IurNusRay 我试过,django3.1, 默认使用 sqlite3 数据库,不开启事务的时候。1000 次,20 并发 sleep 重试是能实现的
    todd7zhang
        9
    todd7zhang  
       Nov 13, 2020
    todd7zhang
        10
    todd7zhang  
       Nov 13, 2020
    如果在外面包一个 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
        11
    todd7zhang  
       Nov 13, 2020
    实测 postgreSQL 没毛病, 1000 个请求,20 并发。https://paste.ubuntu.com/p/pxdPM9B8Cx/

    所有的服务都是直接 runserver
    nonduality
        12
    nonduality  
       Nov 13, 2020
    @todd7zhang 我用 F 表达式,不用 transaction,1000 请求,100 并发(尝试过更高,但受系统限制开不起来),多次测试都完全正常。如果不涉及关键的数值,用 F 表达式足够了,用 transaction 降低数据库性能。
    todd7zhang
        13
    todd7zhang  
       Nov 13, 2020
    @nonduality 这种 filter().update() 主要还是为了防止超售吧,如果你单纯的为了+1, F 表达式确实可以
    About     Help     Advertise     Blog     API     FAQ     Solana     2725 Online   Highest 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 43ms UTC 15:52 PVG 23:52 LAX 08:52 JFK 11:52
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86