乐观锁其实并不是锁。通过SQL的where子句中的条件是否满足来决定是否满足更新条件来更新数据库,通过受影响行数判断是否更新成功,如果更新失败可以再次进行尝试,如果多次尝试失败就返回更新失败的结果。
为了验证乐观锁的使用情况:使用乐观锁时必须设置数据库的隔离级别是Read Committed(可以读到其他线程已提交的数据)。如果隔离级别是Repeatable Read(可重复读,读到的数据都是开启事务时刻的数据,即使其他线程提交更新数据, 该线程读取的数据也是之前读到的数据),乐观锁如果第一次尝试失败,那么不管尝试多少次都会失败。 (Mysql数据库的默认隔离级别是Repeatable Read,需要修改成Read Committed, 可以用 show variables like ‘transaction_isolation’; 来确认数据库事务隔离级别)。
def concurrence(request):
s1 = transaction.savepoint()
print 'begin: {0}'.format(time.time())
retry_time =5
for i in range(retry_time):
db = UserInfo.objects.filter(id=4).first()
old_balance = int(db.password)
new_balance = old_balance - 1
# 用 password 列来模拟余额, 在 sleep 期间把通过 mysql client 修改数据库的值
time.sleep(10)
res = UserInfo.objects.filter(id=4, password=str(old_balance)).update(password=str(new_balance))
if res == 0:
if i == retry_time - 1:
print 'update failed: {0}'.format(time.time())
transaction.savepoint_rollback(s1)
return HttpResponse('update failed')
continue
else:
print 'update successfully: {0}'.format(time.time())
return HttpResponse('update successfully')
return HttpResponse('concurrence test')
视图函数需要注意:
我用 model UserInfo 中的 password 列来模拟用户的余额。
关键的地方是: UserInfo.objects.filter(id=4, password=str(old_balance)).update(password=str(new_balance))
比如本次是取款 1 元,在视图函数中 sleep(10) 的期间,
如果用户的余额发生了变更(比如通过其他的 MySQL Client 连接数据库执行更新操作)即不等于 old_balance 了,那么上述语句的执行就会失败。
就需要去重试来更新.
关于悲观锁的文章,请参考:Django 管理并发操作之悲观锁即select_for_update