V2EX = way to explore
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
V2EX  ›  Python

关于 Django 的乐观锁问题

  •  
  •   IurNusRay · 2020-11-12 15:24:30 +08:00 · 1557 次点击
    这是一个创建于 1505 天前的主题,其中的信息可能已经有所发展或是发生改变。
    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 条回复    2020-11-13 10:42:35 +08:00
    nonduality
        1
    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
        2
    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
        3
    IurNusRay  
    OP
       2020-11-12 16:14:17 +08:00
    @nonduality F 表达式我也试过,也不行
    IurNusRay
        4
    IurNusRay  
    OP
       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
        5
    nonduality  
       2020-11-12 19:03:49 +08:00
    @IurNusRay 我刚测试了一下,用 F 表达式进行数据自加,gunicorn 起 1 个进程跑,用 ab -n 1000 -c 100 测试,完全没问题。所以,用 F 表达式对多数情形下是够用的。
    nonduality
        6
    nonduality  
       2020-11-12 19:20:58 +08:00
    我不太清楚谁提出来“乐观锁”,但看其实现,大概是要保证 filter 到的实例状态具备 origin_read 的值,在此基础上 update 数值,但就算有 trasaction,也无法保证它一定获取到你要的数据状态,自然就 miss 掉了。但 F 表达式不一样,它用 SQL 语句在数据库层面直接操作数据的。
    IurNusRay
        7
    IurNusRay  
    OP
       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
        8
    todd7zhang  
       2020-11-13 09:32:28 +08:00
    @IurNusRay 我试过,django3.1, 默认使用 sqlite3 数据库,不开启事务的时候。1000 次,20 并发 sleep 重试是能实现的
    todd7zhang
        9
    todd7zhang  
       2020-11-13 09:33:32 +08:00
    todd7zhang
        10
    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
        11
    todd7zhang  
       2020-11-13 09:58:33 +08:00
    实测 postgreSQL 没毛病, 1000 个请求,20 并发。https://paste.ubuntu.com/p/pxdPM9B8Cx/

    所有的服务都是直接 runserver
    nonduality
        12
    nonduality  
       2020-11-13 10:07:23 +08:00
    @todd7zhang 我用 F 表达式,不用 transaction,1000 请求,100 并发(尝试过更高,但受系统限制开不起来),多次测试都完全正常。如果不涉及关键的数值,用 F 表达式足够了,用 transaction 降低数据库性能。
    todd7zhang
        13
    todd7zhang  
       2020-11-13 10:42:35 +08:00
    @nonduality 这种 filter().update() 主要还是为了防止超售吧,如果你单纯的为了+1, F 表达式确实可以
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2442 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 16:03 · PVG 00:03 · LAX 08:03 · JFK 11:03
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.