开发网站时经常遇到数据竞争的情况.
比如一个图书馆网站,对于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
- 线程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