flask-sqlalchemy 解决 with_for_update() 行锁不生效、数据滞后问题

2023-11-10

解决方案

我先把解决方案放在前面,后面慢慢阐明拖了我三天的研究过程和原因:
若出现带锁的查询修改前,先行commit一次。

model.db.session.commit()  # 先行commit一次
user = models.User.query.with_for_update().get(user_id)  # 我把所有数据库模型放在model里 所以用model.User取出
user.money -= 1  # 扣费操作
model.db.session.commit()

问题的来源

事实上,数据库行锁是一个很常见的需求。

我高中还在用PHP做一个财务系统的时候,就发现“在相对高并发对用户账户进行扣费”时会出现扣减额不正确的情况,例如每笔订单0.1元,同时发起100笔,理应扣费10元,但系统实际扣费小于10元

在sqlalchemy上的发现

最近,用python开发一套分销系统的时候,再次出现了这个问题,回想高中的解决方案是直接在接口进入的地方对外部文件加锁,这样所有的扣费操作必须在一个订单完成后再进行,但这样显然降低了并发效率,于是我查到了sqlalchemy通行的行锁方案 with_for_update()。

user = models.User.query.filter(
            models.User.username == user_username
            ).with_for_update().first()
# 或者是用get主键的方式直接带锁取出
user = models.User.query.with_for_update().get(user_id)

本以为 这样就轻轻松松解决了,但是,实际发现,依然存在扣费不正确的情况。

同时,这个问题还出现在订单下单后,商品库存应该减少的情况,同样的,这个行锁“没有生效”。

你知道,这是一件非常糟糕的事情,因为这玩意原理就是如此,不过是行锁罢了,锁住这一行数据让其他所有想要查询/修改本行的请求进行等待来保证数据的时效性。但很糟糕,我这样的写法在我的业务中,并没有生效。

资料查询

事实上,遇到这种全网都差不多解决方案但我并没有复现成功的问题,第一反应肯定是我的实现问题。所以我在查询过程中,发现这个行锁和sqlalchemy有另一种写法(或许是原生sqlalchemy的写法):

db.session.query(模型).filter().with_for_update().first()

因为我一直写的是

模型.query.filter().with_for_update().first()

这就很迷呀,难不成是我对sqlalchemy的理解不深然后写法问题导致行锁无法执行?

遂换成db.session的写法,发现依然失败。正好那时候在图书馆,去翻了一下Jack Stouffer的《深入理解Flask》,结果发现其中有这样一段

(Model.query 实际上是 db.session.query(Model))

那么也就是说,并不是写法问题。

进入一筹莫展状态,遂在CSDN上求助@Alien-Hu,很棒的博主,他为我发了两个链接:

https://blog.csdn.net/hxpjava1/article/details/79407961
https://www.lzskyline.com/archives/106/

事实上这两篇文章给我了一些启发,证明我的写法没有问题。

而后,询问NBU某大佬学长(我也不知道他的CSDN账户哈哈),他告诉我跑个helloworld级的项目吧,啊,大佬果然是大佬我怎么没想到,结果就是那么一跑!

from flask import Blueprint, request, current_app, g  # 蓝图
from flask_restful import Api, Resource, reqparse, fields, marshal_with  # restful对象

from server.models import models  # 引入数据库文件
import time  # time库
import threading  # 线程库

hello_bp = Blueprint('hello_bp', __name__)

api = Api(hello_bp)

channel_id = 35


class Hello(Resource):
    def get(self):
        start_time = time.time()
        order = models.db.session.query(models.Order).with_for_update().get(583)
        channel = models.Channel.query.with_for_update().get(channel_id)
        channel.inventory -= 1
        # time.sleep(0.1)
        models.db.session.commit()
        end_time = time.time()
        print("扣减后库存:", channel.inventory, "结束时间:", end_time, "消耗时间:", end_time - start_time)
        return {"status": 200}


# channel_raw = models.Channel.query.get(channel_id)
# print("开始扣减之前库存:", channel_raw.inventory)
# for i in range(1, 20):
#     my_thread = threading.Thread(target=sub_inventory, args=(i,))
#     my_thread.start()

api.add_resource(Hello, '/hello')

运行结果:
在这里插入图片描述

哇哦,居然行锁生效的。并且,若是其中增加time.sleep()效果更佳明显,因为你会发现不同进程的等待时间是不一样的,基本正好差sleep的时间,也就是行锁是生效的。

那就更迷了呀

这意味着行锁是生效的,但数据确滞后更新了(我print出了每次扣费前后的情况,发现不同进程有一定概率是相同数值,也就是出现了看起来像行锁没有锁住的情况)。

在扣减库存的时候出现了同样的问题:
在扣减库存的时候出现了同样的问题
你知道,事实上在这里的update是一个先查出来再修改回去的过程,那么有没有可能是查的时候的数据问题?遂意识到,这似乎是一个数据滞后的缓存问题,于是,这时候出现了一篇文章:

http://muhongqiao.top/?id=380

它遇到的是查询的时候有概率碰到数据滞后的情况,并且提供了解决方案:

1、每次查询后进行commit操作
2、创建connect连接时,设autocommit=True,自动进行commit提交。下面是flask中的设置例子:

class DevelopmentConfig(Config):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = 'mysql://root:12333@127.0.0.1:3306/test?charset=utf8mb4&autocommit=true'

但问题在于,我需要行锁,也就是查出来不能直接commit呀,直接commit就把锁解开了。

我注意到这个博主穆琪的文章中有这样一段话:

经过搜索的原因如下:
SQLAlchemy 带有对象缓存机制,在重复查询相同的对象时,直接先查询本地的缓存,而不需要从数据库加载数据。
而且SQLAlchemy并没有什么参数开关设置关闭缓存,那么一定是有深层次的原因。
原因的核心是InnoDB的事务隔离。
InnoDB 的默认隔离级别。它可以防止任何被查询的行被其他事务更改,从而阻止不可重复的读取,而不是 幻读取。它使用中度严格的锁定策略,以便事务内的所有查询都会查看同一快照中的数据,即数据在事务开始时的数据。
当创建查询事务时,事务一直没有进行更新,每次查询到的数据都是之前查询结果的快照,所有才会出现多进程时候数据不同步的情况。

这意味着,sqlalchemy是存在一个缓存机制的!

拨云见日有没有,知道了缓存问题的存在,那就很好解决了,我们要解决缓存问题。如何解决缓存问题?穆琪博主提到的commit,那直接就在带锁的查询之前加一句

db.session.commit()

就好了嘛!

于是问题就此解决,后来又回去看我带锁查询之前的业务逻辑,发现其中有通过外键关联对我要锁的表进行过查询,我怀疑就是在这个时候sqlalchemy的缓存机制把数据存下来了,使得我业务环境中的效果和helloworld级代码中的效果不一样。遂修改业务中所有遇到的行锁问题,解决了这个所谓“不生效”问题。

另外,关于表锁

其实还有一个问题没有解决,或者说解决起来很奇怪。

打个比方,在用户下单之前,我需要判断这个用户当日下的单数有没有超出限额,于是我需要查询order表。

这个时候,若有多条订单同时进入,显然可能会被同时加入数据库,但我又不可能锁掉整个order表(因为还有其他对order的业务存在),于是我重新建了一个表,在那个表里面新建了一条。每次要判断限额的时候,我去锁那条数据,这样每次的判断都是串行的不会冲突。

然鹅不幸的事情又发生了,即使我通过time.sleep()判断行锁的确生效了的情况,而且若有50ms的延迟情况下,这个机制运行得很可靠,但我一旦删掉了这50ms的延迟,就开始同样出现数据滞后的情况了。

无奈之下,我保留了那50ms的延迟,但带来的副作用意味着每秒最多下20个订单。

从以前以为CSDN是个什么垃圾下载站,到倍感博主们为后人开路的精神,到我现在也开始记录这些坑,真是有趣,哈哈。

若看我这篇文章的读者对我上述问题有好的解决方案,欢迎和我聊聊哇!

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

flask-sqlalchemy 解决 with_for_update() 行锁不生效、数据滞后问题 的相关文章

  • 从Django中具有外键关系的两个表中检索数据? [复制]

    这个问题在这里已经有答案了 This is my models py file from django db import models class Author models Model first name models CharFie
  • python multiprocessing 设置生成进程等待

    是否可以生成一些进程并将生成进程设置为等待生成的进程完成 下面是我用过的一个例子 import multiprocessing import time import sys def daemon p multiprocessing curr
  • 随机组合 MySQL 数据库中的两个单词

    我有一个包含名词和形容词的数据库 例如 id type word 1 noun apple 2 noun ball 3 adj clammy 4 noun keyboard 5 adj bloody ect 我想创建一个查询 它将抓取 10
  • Python 3d 绘图设置固定色阶

    我正在尝试绘制两个 3d 数组 第一个数组的 z 值在范围内 0 15 0 15 第二个来自 0 001 0 001 当我绘图时 色标自动遵循数据范围 如何设置自定义比例 我不想看到 0 001 的浅色 而应该看到 0 15 的浅色 如何修
  • 为什么 web2py 在启动时崩溃?

    我正在尝试让 web2py 在 Ubuntu 机器上运行 所有文档似乎都表明要在 nix 系统上运行它 您需要下载源代码并执行以下操作 蟒蛇 web2py py 我抓住了source http www web2py com examples
  • PyQt 使用 ctrl+Enter 触发按钮

    我正在尝试在我的应用程序中触发 确定 按钮 我当前尝试的代码是这样的 self okPushButton setShortcut ctrl Enter 然而 它不起作用 这是有道理的 我尝试查找一些按键序列here http ftp ics
  • Spark SQL 中的 SQL LIKE

    我正在尝试使用 LIKE 条件在 Spark SQL 中实现联接 我正在执行连接的行看起来像这样 称为 修订 Table A 8NXDPVAE Table B 4 8 NXD V 在 SQL Server 上执行联接 A revision
  • 打印数字时添加千位分隔符[重复]

    这个问题在这里已经有答案了 我真的不知道这个问题的 名称 所以它可能是一个不正确的标题 但问题很简单 如果我有一个数字 例如 number 23543 second 68471243 我想要它使print 像这样 23 54368 471
  • 快速将列的副本添加到 MySQL 表

    我需要一种快速的方法来复制表中的 DATETIME 列并为其指定一个新名称 我的表中有一个名为 myDate 的列 名为 myResults 我需要一个查询来在名为 newDate 的表中创建一个新列 该列的数据与 myDate 列完全相同
  • Python 内置的 super() 是否违反了 DRY?

    显然这是有原因的 但我没有足够的经验来认识到这一点 这是Python中给出的例子docs http docs python org 2 library functions html super class C B def method se
  • 如何使用 Selenium 和 ChromeDriver 解决 TypeError: 'module' object is not callable 错误 [重复]

    这个问题在这里已经有答案了 代码试验 from selenium import webdriver from selenium webdriver chrome options import Options as Chromeoptions
  • 尽管我已在 python ctypes 中设置了信号处理程序,但并未调用它

    我尝试过使用 sigaction 和 ctypes 设置信号处理程序 我知道它可以与python中的信号模块一起使用 但我想尝试学习 当我向该进程发送 SIGTERM 时 但它没有调用我设置的处理程序 只打印 终止 为什么它不调用处理程序
  • 如何将 ascii 值列表转换为 python 中的字符串?

    我在 Python 程序中有一个列表 其中包含一系列数字 这些数字本身就是 ASCII 值 如何将其转换为可以在屏幕上回显的 常规 字符串 您可能正在寻找 chr gt gt gt L 104 101 108 108 111 44 32 1
  • Python GTK+ 画布

    我目前正在通过 PyGobject 学习 GTK 需要画布之类的东西 我已经搜索了文档 发现两个小部件似乎可以完成这项工作 GtkDrawingArea 和 GtkLayout 我需要一些基本函数 如 fillrect 或 drawline
  • python 中的“槽包装器”是什么?

    object dict 和其他地方的隐藏方法设置为这样的
  • pandas.read_csv 将列名移动一倍

    我正在使用位于的 ALL zip 文件here http www fec gov disclosurep PDownload do 我的目标是用它创建一个 pandas DataFrame 但是 如果我跑 data pd read csv
  • 如何在 Flask 中的视图函数/会话之间传递复杂对象

    我正在编写一个 Web 应用程序 当 且仅当 用户登录时 该应用程序从第三方服务器接收大量数据 这些数据被解析为自定义对象并存储在list 现在 用户在应用程序中使用这些数据 调用不同的视图 例如发送不同的请求 我不确定什么是最好的模式在视
  • pytest找不到模块[重复]

    这个问题在这里已经有答案了 我正在关注pytest 良好实践 https docs pytest org en latest explanation goodpractices html test discovery或者至少我认为我是 但是
  • python 对浮点数进行不正确的舍入

    gt gt gt a 0 3135 gt gt gt print 3f a 0 314 gt gt gt a 0 3125 gt gt gt print 3f a 0 312 gt gt gt 我期待 0 313 而不是 0 312 有没有
  • SQL 更新 - 更新选定的行

    我正在使用 SQL Server 2008 我有一个名为MYTABLE有两列 ID STATUS 我想编写一个存储过程来返回其记录STATUS是 0 但是这个存储过程必须更新STATUS返回行数为 1 如何在单个查询中执行此选择和更新操作

随机推荐

  • java根据生日计算年龄工具类

    在开发中时常遇到要通过生日计算年龄的需求 这里记录一下 private static int getAgeByBirth Date birthday int age 0 try Calendar now Calendar getInstan
  • linux强制卸载mysql报错

    error Failed dependencies mysql community client x86 64 gt 8 0 11 is needed by installed mysql community server 8 0 31 1
  • html在线校验器,HTML validate HTML验证

    HTML validate是指HTML验证 它是通过与标准HTML规则进行比较的方式 分析HTML文档 标记出错误和非标准代码的处理过程 Web页面使用HTML进行渲染 而HTML本身采用了HTML规范作为其规则和标准 通过验证HTML代码
  • 驱动开发 作业 day9 9/20

    基于platform实现 head h ifndef HEAD H define HEAD H 构建LED开关的功能码 不添加ioctl第三个参数 define LED ON IO l 1 define LED OFF IO l 0 end
  • 同时安装cuda8和cuda9

    转载自 https blog csdn net lovebyz article details 80704800 为了使用tensorflow目标检测API的所有算法 所以打算升级一下CUDA版本以支持tf gpu 1 5 但原本项目都是基
  • HTML5 canvas标签-1 基本使用

    终于有空使用csdn和大家分享点自己平时学习工作时候的心德啦 第一步 介绍下canvas的基本使用 首先 因为canvas是html5的一个标签 所以保险起见 可以先确认下canvas是否兼容 try document createElem
  • VISIO中的工具栏、菜单栏丢失的原因及解决办法

    症状 在 Microsoft Visio 中 您可能会遇到一个或多个以下症状 您所了解的工具栏是启用是不可见的 找不到菜单栏 按钮从工具栏中消失了
  • k8s搭建部署3个节点服务器

    目录 一 环境准备 二 安装kubeadm kubelet和kubectl 三 部署kubernetes Master 四 node节点加入集群 五 查看集群的状态 Init ImagePullBackOff错误 swap分区没有关闭 没有
  • 华为OD机试真题2023(JavaScript)

    华为机试题库已于5月10号由2022Q4 A卷 切换 为2023 B卷 B卷区地址 华为OD机试真题2023 B卷 JS 华为机试有三道题目 第一道和第二道属于简单或中等题 分值为100分 第三道为中等或困难题 分值为200分 总分为400
  • matlab 平方差,第七章 最小平方差的方法(The Method of Least Square ).doc

    第七章 最小平方差的方法 The Method of Least Square doc 第七章 最小平方差的方法 The Method of Least Square 本章說明利用最小平方差法 找出適合一組資料的曲線 在本章中包含Matla
  • T100客户端接口开发实例(T100对接销售易CRM)

    design bu leezec 296066606 应用实例 T100销售订单推送到CRM 需求分解 1 调用销售易接口校验取得token 2 取得token后按照要求推送数据 1 什么是token 首先 要调用CRM的实际应用场景 就绕
  • ISula 容器学习历程

    容器统一架构图 转自ISula容器引擎 openeuler org 1 下载安装 iSulad容器的下载安装只需要很简单的一条命令 yum install y iSulad 安装完成 2 容器配置 以配置nginx服务器为例 2 1 启动i
  • python 算法基础

    目录 基础算法 一元回归算法 多项式回归 预测 任意函数回归 分类算法 kNN k最邻近算法 聚类算法 k means DBSCAN 推荐算法 暂时不写 降维算法 数据预处理 主成分分析 PCA 因子分析 FactorAnalysis sk
  • 浅谈Web用户体验(一)

    最近一段时间在接触web用户体验 我认为 如果要鼓励用户去使用一个新的网站或软件 首先要做到的是 把用户需要完成的事的难度尽量降低 因为一般用户是没有耐性 有点懒的去做复杂的事的人 要解决这些问题 下面总结了一些方法 第一 尽量告诉用户需要
  • TightVNC H264编解码(二)之硬编码库的编译

    AVCodec codec avcodec find encoder by name nvenc h264 如果是默认的ffmpeg库 返回结果是NULL 看来是不带有硬编码功能的 重新编译分支ffnvcodec 不到半个小时编译完成 返回
  • Git 常用命令 --- git push命令

    git push的一般形式为 git push lt 远程主机名 gt lt 本地分支名 gt lt 远程分支名 gt 例如 git push origin master refs for master 即是将本地的master分支推送到远
  • golang配置国内镜像

    点击进入 golang中文网
  • elementUI图片遍历循环+预览【el-image查看大图+预览】

    我这边接受的数据结构 code 200 msg 图片列表 object id 1 partsName picturesName a jpg picturesUrl http 192 168 3 65 8111 image a jpg typ
  • POI向Excel中插入图片

    package com xiangyu bigdata xycom execl import java awt image BufferedImage import java io ByteArrayOutputStream import
  • flask-sqlalchemy 解决 with_for_update() 行锁不生效、数据滞后问题

    解决方案 我先把解决方案放在前面 后面慢慢阐明拖了我三天的研究过程和原因 若出现带锁的查询修改前 先行commit一次 model db session commit 先行commit一次 user models User query wi