前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >记我的小网站发现的Bug之一 —— 某用

记我的小网站发现的Bug之一 —— 某用

作者头像
py3study
发布2020-01-22 12:20:16
4220
发布2020-01-22 12:20:16
举报
文章被收录于专栏:python3python3

1.故事背景

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

事故现场
事故现场

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

2.查找问题

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

代码语言:javascript
复制
@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,就拿这个测测

代码语言:javascript
复制
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,还不知道它加锁得怎么搞,先查一下资料。 函数的定义如下:

代码语言:javascript
复制
@_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字段,所以应该使用互斥锁。 修改后代码如下:

代码语言:javascript
复制
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的了解,同时对并发锁有了更直观的理解。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019-05-06 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.故事背景
  • 2.查找问题
  • 3.确定问题
  • 4.解决问题
  • 5.心得
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档