一种利用pending状态实现锁的方案
问题背景
开发网站时经常遇到数据竞争的情况.
比如一个图书馆网站,对于Book1
学生还书,把Book1的status变成可借阅
book = Book1.objects.get(id=1) # 初始 use, naem, 错别字
book.status = "free"
book.save() # free naem
管理员修改名称
book = Book1.objects.get(id=1) # 初始 use, naem, 错别字
book.name = "name"
book.save() # use, name
无论是学生的对象先保存还是管理员的对象先保存, 都会导致book的status或者name有一个有问题.
已有解决方案的缺陷
添加锁
一旦加锁意味着所有拿Book, 改Book的地方都要加锁. 对于已有项目改动工作量太大了. 比如每天对书本进行统计要加锁
每次借阅书籍后保存书籍的借阅次数需要加锁
修改书本的标签号需要加锁
给书本添加标签要加锁(利用arrayfield直接保存在书本的标签)
只允许改里面部分字段
和加锁一样,改动也很大.每个保存book的代码都要把status排除. 并且django-rest-framework的serializer还不支持update_fields
利用pending状态实现锁
原理
线程A和线程B占用对象Book1时, 创建一个占用记录
occupy_A/B = BookOccupy.objects.create(status="pending", book=Book1)
占用完毕后查看处于pending的记录是否存在
pending = BookOccupy.objects.filter(status="pending", book=Book1).exclude(id=occupy_A).first()
# 顺序很重要,先看pending后看using. 避免看完using后正好有个pending的状态变成using
using = BookOccupy.objects.filter(status="using", book=Book1).exclude(id=occupy_A).first()
如果已经被其他人占用了就退出
if using:
return
如果有其他pending,就取消占用随机等待后重拾
if pending:
occupy_A/B.delete()
time.sleep(random.random())
occupy_A/B = BookOccupy(status="pending", book=Book1)
证明
可以看到对于每次占用都有空-pending-using的状态. 任意状态下,
A状态 |
B状态 |
A操作 |
B操作 |
|---|---|---|---|
None |
Any |
进入pending |
Continue |
Pending |
None |
进入using |
进入Pending |
Pending |
Pending |
进入None随机等待 |
进入None随机等待 |
Pending |
Using |
Exit |
Using |
Using |
Any |
Using |
Exit |
具体实现
数据库实现
上面的原理里就是利用数据库的ORM实现.redis缓存实现
利用using:key保存已经占用的. 利用pending:set保存等待的
while True:
occupy_A/B = redis.sadd("set", "id_A/B")
if redis.scard("set") >= 2:
redis.srem("set", "id_A/B")
time.sleep(random.random())
continue
if redis.get("using:key"):
return None
# 没有using, 没有pending
redis.set("using:key", "id_A")
redis.srem("set", "id_A")
文件系统实现
创建2个文件夹, pending和using. 每次都查看using文件夹里是否有文件, pending文件夹的数量是否大于1