1.故事背景

今天上午我忙完手中的事情之后突然想起来我还没签到,于是赶紧打开签到页面,刚点击了签到按钮,提示“签到成功,获得25阅读额度!”,正准备退出浏览器,忽然发现签到列表有异常,居然有用户有两条签到记录!!!

难道我的代码又出Bug了???不可能!!!

2.查找问题

不过保险起见,还是去检查了一下代码。

代码如下:

@app.route('/api/sign', methods=['POST'])
@is_authenticated
def api_sign():
id = current_user.id
if current_user.is_sign:
return jsonify({'status':0,'message':'今日已签到,请明天8点再来签到'})
else:
pass

我在用户信息上放了一个is_sign字段表示当天该用户是否有签到,然后在每天8点的时候通过linux的定时任务更新所有用户的这个字段为False,在用户签到的时候,会首先检查这个字段,如果为False就会执行签到逻辑,然后会把这个字段更新为True,我感觉这个逻辑应该没啥问题。

一时陷入僵局

遂决定先去查查nginx的log,看看请求信息,费了九牛二虎之力,终于把日志文件下载了下来,阿里云1M小水管可太慢了,然后因为前两天分了站点来归档log,忘了做日志切割,整个日志文件有17M之巨,压缩完也下了好久。

根据此用户签到时间,找到了当时的请求记录



通过日志,可以看到连续post了三条,不知道是因为浏览器卡了还是因为这个用户有点意思,先不去纠结这些细枝末节,解决问题更重要。

3.确定问题

看到这个日志我大概明白了,应该是并发没有加锁背锅。

写点代码测试一下,python有个并发库叫grequests,就拿这个测测

import grequests
import requests
if __name__ == '__main__':
urls=[
'http://192.168.48.129/api/sign',
'http://192.168.48.129/api/sign',
'http://192.168.48.129/api/sign',
'http://192.168.48.129/api/sign',
'http://192.168.48.129/api/sign',
'http://192.168.48.129/api/sign',
]
cookies = dict(session='xxxxxxx')
rs = (grequests.post(u,cookies=cookies,data=dict(card_id=1)) for u in urls)
resp = grequests.map(rs)
for r in resp:
print(r.json())

果然,前四次都签到成功了!

只成功四次是因为我是用uWSGI部署得站点,然后配置了processes = 4,只有四个进程处理请求,所以轮到后两个请求得时候,is_sign已经是True

用户签到的逻辑如下:

  • 插入一条签到记录
  • 修改阅读额度表,为用户增加额度
  • 插入一条额度变更记录
  • 提交修改

正常来说,如果是不同用户操作的,即使并发了对业务来说不会有任何问题,因为每个人都操作的是自己的数据,不会产生错误数据。

但是,今天遇到的是单用户并发了。

emmm,只能说这个老哥有点东西。

4.解决问题

不过既然发现了问题,那就得解决掉它。

orm框架我用的是Flask-SQLAlchemy,还不知道它加锁得怎么搞,先查一下资料。

函数的定义如下:

@_generative()
def with_for_update(self, read=False, nowait=False, of=None):
"""return a new :class:`.Query` with the specified options for the
``FOR UPDATE`` clause. The behavior of this method is identical to that of
:meth:`.SelectBase.with_for_update`. When called with no arguments,
the resulting ``SELECT`` statement will have a ``FOR UPDATE`` clause
appended. When additional arguments are specified, backend-specific
options such as ``FOR UPDATE NOWAIT`` or ``LOCK IN SHARE MODE``
can take effect. E.g.:: q = sess.query(User).with_for_update(nowait=True, of=User) The above query on a Postgresql backend will render like:: SELECT users.id AS users_id FROM users FOR UPDATE OF users NOWAIT .. versionadded:: 0.9.0 :meth:`.Query.with_for_update` supersedes
the :meth:`.Query.with_lockmode` method. .. seealso:: :meth:`.GenerativeSelect.with_for_update` - Core level method with
full argument and behavioral description. """

read:是标识加互斥锁还是共享锁. 当为 True 时, 即 for share 的语句, 是共享锁. 多个事务可以获取共享锁, 互斥锁只能一个事务获取. 有"多个地方"都希望是"这段时间我获取的数据不能被修改, 我也不会改", 那么只能使用共享锁.

nowait :其它事务碰到锁, 是否不等待直接"报错".

of:指明上锁的表, 如果不指明, 则查询中涉及的所有表(行)都会加锁.

这里需要对用户信息表进行修改,要更新is_sign字段,所以应该使用互斥锁。

修改后代码如下:

def api_sign():
id = current_user.id
_user_info = user_info.query.filter_by(id=id).with_for_update().first()
if _user_info.is_sign:
return jsonify({'status':0,'message':'今日已签到,请明天8点再来签到!'})
else:
pass

再次执行上面的并发请求代码,现在就只有第一次签到成功了。

问题成功解决!

5.心得

通过对这次问题的解决,加深了对SQLAlchemy的了解,同时对并发锁有了更直观的理解。

记我的小网站发现的Bug之一 —— 某用户签到了两次的更多相关文章

  1. 记一次小团队Git实践(下)

    在上篇中,我们已经能基本使用git了,接下来继续更深入的挖掘一下git. 更多的配置自定义信息 除了前面讲的用户名和邮箱的配置,还可以自定义其他配置: # 自定义你喜欢的编辑器,可选 git conf ...

  2. 如何写出一个让人很难发现的bug?

    程序员的日常三件事:写bug.改bug.背锅.连程序员都自我调侃道,为什么每天都在加班?因为我的眼里常含bug. 那么如何写出一个让(坑)人(王)很(之)难(王)发现的bug呢? - 1 -新手开发+ ...

  3. 浅谈如何写出一个让(坑)人(王)很(之)难(王)发现的bug

    该文章内容来自脚本之家,原文链接:https://www.jb51.net/news/598404.html 程序员的日常三件事:写bug.改bug.背锅.连程序员都自我调侃道,为什么每天都在加班?因 ...

  4. 如何隐藏一个让人很难发现的bug?

    程序员的日常三件事:写bug.改bug.背锅.连程序员都自我调侃道,为什么每天都在加班?因为我的眼里常含bug. 那么如何写出一个让(坑)人(王)很(之)难(王)发现的bug呢? - 1 - 新手开发 ...

  5. 记一次小团队Git实践(中)

    对于初学者,从使用上先入手,往往学的最快,并从中汲取教训,再回头更深入的学习,效果尤佳. 安装git 安装git自不必说,mac已经内置了git,linux下一个命令就能搞定,windows下需要下载 ...

  6. 最近用django做了个在线数据分析小网站

    用最近做的理赔申请人测试数据集做了个在线分析小网站. 数据结构,算法等设置都保存在json文件里.将来对这个小破站扩充算法,只修改一下json文件就行. 当然,结果分析还是要加代码的.页面代码不贴了, ...

  7. 小程序背景图片bug

    在pc端调试的时候已经可以看到出现背景图片了,但是在真机调试的时候却发现没有背景图片,那么原因是什么呢?真机调试和vconsole也看不出什么鸟,其实这是小程序的一个bug.另一种说法是:backgr ...

  8. Django工程的建立以及小网站的编写

    这篇博文会详细的介绍如何创建django工程,介绍我如何做了第一个网站.本文基于windows7安装了python2.7.12,Django1.8.18(LTS)版.采用的IDE为pycharm.建议 ...

  9. 【踩坑经历】一次Asp.NET小网站部署踩坑和解决经历

    2013年给1个大学的小客户部署过一个小型的Asp.NET网站,非常小,用的sqlite数据库,今年人家说要换台服务器,要重新部署一下,好吧,虽然早就过了服务时间,但无奈谁叫人家是客户了,二话不说,上 ...

随机推荐

  1. DRF教程6-分页

    rest框架提供自定义分页样式,让你修改再每个页面上显示多少条数据, pagination API 可以: 分页链接作为响应内容的一部分 分页链接包含在响应头里,比如Content-Range or  ...

  2. AKOJ-1695-找素数

    题意: 给定区间L,R. 计算区间中素数个数. 2 <= L,R <= 2147483647, R-L <= 1000000. 思路: 素数区间筛 先筛(2-sqrt(r)). 再用 ...

  3. vue 中的router 配置问题 导致的内存溢出~~~

    最近的项目用到 vue, 各种踩坑中. 其中一个就是router映射表写的稍有不慎,就会出现内存溢出的问题, 而且也不会具体告诉你哪里出错,所以很是头疼~~~ 出错多了,发现了一些router的一些规 ...

  4. LM358与TL431验证

  5. 17997 Simple Counting 数学

    17997 Simple Counting 时间限制:2000MS  内存限制:65535K提交次数:0 通过次数:0 题型: 编程题   语言: 不限定 Description Ly is craz ...

  6. node项目 Error: Cannot find module 'mongoose'

    这是因为你部署的项目没有添加mongoose,使用 在自己项目的根目录下:npm install mongoose --save

  7. js 跨浏览器实现事件

    我们知道不同的浏览器实现事件是不同的,就比如说我们常见的有三种方法: 1,dom0处理事件的方法,以前的js处理事件都是这样写的. (function () { var p=document.getE ...

  8. Java编程基础-字符串

    在Java语言中,字符串数据实际上由String类所实现的.Java字符串类分为两类:一类是在程序中不会被改变长度的不变字符串:另一类是在程序中会被改变长度的可变字符串.Java环境为了存储和维护这两 ...

  9. 海康威视采集卡结合opencv使用(两种方法)-转

    (注:第一种方法是我的原创 ^_^. 第二种方法是从网上学习的.) 第一种方法:利用 板卡的API:  GetJpegImage 得到 Jpeg 格式的图像数据,然后用opencv里的一个函数进行解码 ...

  10. java.lang.UnsatisfiedLinkError: dlopen failed: /data/app/xxx/lib/arm/liblame.so: has text relocations

    最近在写本地录音转码过程中引入了liblame.so,我这边用了不同系统版本的手机测试本地录音都没有出现问题,但是有一天,同事在测试的时候,出现了以下错误: 09-13 17:32:29.140 26 ...