redis 事务

redis的事务和传统的关系型数据库不同,在关系型数据库中,用户首先向数据库发送一个BEGIN信号,然后执行各个相互一致的读写操作,最后,用户发送COMMIT来确认之前的操作,或者发送ROLLBACK来放弃之前的操作。

在redis中也有简单的方法可以处理一连串的读写操作,使用特殊命令MULTI为开始,然后传入一连串用户的操作,最后以EXEC结束,

但这种做法,实际上是在用户执行EXEC之前,客户端缓存保留所有命令,在EXEC之后一次性把所有命令发送到服务器执行,然后等待直到接收所有的命令回复为止,所以用户没办法根据读取到的数据来做决定是否rollback和commit。

但也正是因为它一次性传输所有命令的方式,可以将N条命令造成的N次网络往返,浓缩到1次网络往返,所以性能得到了提高

但是,考虑这样一个情况:我们的商品a在redis里只剩2件,此时有两个客户都在购买它,分别是客户端A和客户端B,此时它们俩都执行命令:

> GET a
2
> MULTI
ok
> ..... do something
> SET a 1
ok
> EXEC
ok

如果此时两个客户端在同时购买了此商品,那么可能会导致此时a被更新为1,而不是0,而这明显是错误的。

如何解决这个问题?

redis通过WATCH命令实现了乐观锁机制。

乐观锁:

客户端在每次修改操作时,乐观的以为不会出现冲突,执行之后,如果产生了冲突,则再次重新执行,直到成功为止。

乐观锁的缺点是当多个客户端修改同一条数据时,可能会导致多个客户端不断失败并重试,耗费大量时间

相对应的,也有悲观锁,一般出现在关系型数据库(如mysql)中:

客户端在执行写操作时,悲观地认为一定会产生冲突,所以直接对该数据进行加锁,以保证自身一定能执行成功。

悲观锁的缺点是当多个客户端修改同一条数据时会进行锁竞争和释放,没有获得额锁的客户端需要等待锁释放,如果排在前面的客户端执行非常慢,则会使后续客户端的等待时间变长。


使用WATCH命令,redis会监视执行命令时a的值,在执行EXEC时,检查a的值是否有变动,如果发现被修改,则事务失败。

所以回到刚才的问题,当两个客户同时购买商品a时,我们的命令可能是这样的:

> WATCH a
ok
> GET a
2
> MULTI
ok
> ......do something
> SET a 1
ok
> EXEC
nil

此时,客户端B在事务执行之前,a已经被客户端A修改,则会EXEC会返回nil,表示事务失败,此时客户端B再去GET a,会得到a已经变为1的结果。(此时客户端A执行成功,客户端B执行失败)

这样,我们通过WATCH命令,保证了数据的一致性。