SQLAlchemy并发写入引发的思考
背景
近期公司项目中加了一个积分机制,用户登录签到会获取登录积分,但会出现一种现象就是用户登录时会增加双倍积分,然后生成两个积分记录。此为问题
问题分析
项目采用微服务架构,下图为积分机制流程
worker通过分析日志记录从而判断用户当天积分是否增加,进而进行积分增加增添记录或者无操作。
两个worker对积分数据库进行同时写入,造成积分双倍增加的情况,那问题找到了,就是对数据库并发写入的问题。
解决方法,加锁
锁
- 共享锁
- 定义:共享锁就是允许多个线程同时获取一个锁,一个锁可以同时被多个线程拥有。
-
举例:比如有一个房间,你和你的女朋友都有要是可以进入这个房间,这就是共享锁,这个房间是你跟你的女朋友共享的
- 互斥锁
- 定义:互斥锁,也称作独占锁,排它锁,一个锁在某一时刻只能被一个线程占有,其它线程必须等待锁被释放之后才可能获取到锁。
- 举例:就比如蹲厕所吧,同一时间一个茅坑由你一个人独占,这就叫做独占锁,互斥锁,其他人是不可以用的(当然,特殊情况除外)
- 乐观锁
- 总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
-
悲观锁
- 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
当然,这次解决问题采用的是悲观锁,互斥锁
SQLAlchemy
项目是采用SQLAlchemy对mysql进行操作,题目是SQLAlchemy引发的思考,当然就少不了SQLAlchemy这个主角。SQLAlchemy 提供 with_for_update 函数 进行 锁的操作 .
session.Query(User).with_for_update().first()
session.Query(User).with_for_update(read=True).first()
完整形式为:
with_for_update(read=False, nowait=False, of=None)
read 是标识加互斥锁还是共享锁. 当为 True 时, 即 for share 的语句, 是共享锁. 多个事务可以获取共享锁, 互斥锁只能一个事务获取. 有"多个地方"都希望是"这段时间我获取的数据不能被修改, 我也不会改", 那么只能使用共享锁. nowait 其它事务碰到锁, 是否不等待直接"报错". of 指明上锁的表, 如果不指明, 则查询中涉及的所有表(行)都会加锁.
补充
在用SQLAlchemy对数据库操作的过程中出现这样一个现象,当在session.query()时,如果之前有session.add()操作,即使尚未进行commit操作,在query时也会查询到这个尚未真正插入数据库的对象。这个有待探究 ,后续补充
我的小鱼你醒了
还认识早晨吗
昨夜你曾经说
愿夜幕永不开启