面试的时候遇到过问Redis是如何解决“竞态条件”的,相关知识点总结一下。
乐观锁
所谓竞态条件,举个例子,一个代表点击数的数值hitcount,每个客户点击一次则+1。
没有事务的时候,假设我们的操作如下:
hc=GET hitcount;
hc=hc+1;
SET hitcount $hc;
非并发状态下,这样做是OK的,但是并发状态下会出现的问题是:
A和B两个客户端分别从Redis处取值,并+1,值都是11。
Redis是单线程模型,所以A和B的SET命令只能先执行1个,此处先执行A,hitcount更新为11。
接着执行B的SET命令,hitcount依然是11,这就是明显的因为竞态而产生的错误,hitcount应该为12才是。
Redis的事务其实是通过MULTI命令开启事务,将后续一系列的命令放在一个队列里,不立即执行,直到EXEC命令,队列中的命令才会依次执行。
命令类似:
MULTI;
set val1 111;
set val2 222;
EXEC;
在实际工作中,我们也会经常遇到这种问题:我们必须先拿到数据,根据数据做出判断,进行一些处理之后,再更新数据,这时候我们就没法保证取数据、更新数据在同一个队列事务中了。
这就需要乐观锁。简单说,我们每取一个数据的时候,Redis不仅返回数值,还会返回这个数值的版本号。
当我们执行更新命令时,Redis会拿你要SET值的版本号与库里现在值的版本号进行比对,如果相同,则更新,版本号变更。
如果版本号不同,则说明在我们执行更新命令之前,有其他客户端修改了这条数据,我们的更新操作失败。
Redis里是通过WATCH命令来监控版本号的。
WATCH hitcount;
hc=GET hitcount;
hc=hc+1;
MULTI;
SET hitcount $hc;
EXEC;
因为通过WATCH监控了hitcount这个Key,那么在事务中SET的时候,一旦发现版本号不对,执行就失败。
悲观锁
乐观锁是CAS——Check And Set,先检查(版本号)再设置更新,那么悲观锁就是先锁,再查,最后更新。
以MySQL为例,我们要实现悲观锁:
SELECT * FROM tabA AS t WHERE t.id=1 FOR UPDATE;
这样,因为主键是明确指定的(id=1),所以这一行记录就被锁定了——行锁定,在本事务被commit之前,这条数据都只会被本事务的SQL语句进行修改,其他事务的加锁操作,更新操作都会在本事务提交之后再执行,单纯的查询则不受影响。
如果没有指定明确地主键,则锁定的是表——表锁定。