Redis中的事务本质是一组命令的集合。事务中的每条命令都会被序列化,执行过程中按顺序执行,不允许其他命令进行干扰。提炼一下即:(1)一次性(2)顺序性(3)排他性。
值得注意的是:Redis事务没有隔离级别的概念;此外,Redis单条命令是保证原子性的,但是事务不保证原子性。
Redis事务操作过程
- 开启事务(
multi ) - 命令入队
- 执行事务(
exec )
因此开启事务以后的所有命令,在加入队列时都没有被执行,而是直到提交时才会开始执行(exec),并一次性执行完毕。
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> get k2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> keys *
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) OK
3) "v2"
4) OK
5) 1) "k2"
2) "k3"
3) "k1"
取消事务(discard )
与exec相反,discard 命令可以撤销事务中的所有命令,使其不执行。
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> get k1
QUEUED
127.0.0.1:6379(TX)> discard
OK
127.0.0.1:6379> exec
(error) ERR EXEC without MULTI
127.0.0.1:6379> get k1
(nil)
可见上述代码中,set的k1和k2,由于使用了discard 放弃了事务,因此所有的命令都没有得到执行。
事务错误
事务的错误分为两种:代码语法错误(编译时异常),代码逻辑错误(运行时异常)。
(1)编译时异常:事务中所有的命令都不执行
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> this is an error command
(error) ERR unknown command `this`, with args beginning with: `is`, `an`, `error`, `command`,
127.0.0.1:6379(TX)> get k2
QUEUED
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k1
(nil)
可见事务中一旦出现代码语法错误,则事务中所有的命令都不执行。
(2)运行时错误:该条命令错误执行,而其他命令正常执行(因此Redis事务不保证原子性)
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> incr k1
QUEUED
127.0.0.1:6379(TX)> get k1
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) OK
3) (error) ERR value is not an integer or out of range
4) "v1"
127.0.0.1:6379> get k2
"v2"
可见上述代码中,由于k1对应的值是字符串类型的v1,所以incr命令会出现运行时错误。但运行时错误只对这一条单独的命令有影响,而事务中其他的命令仍然可以正常执行。这说明了并不能保证Redis事务的原子性。
监控(可用作乐观锁的实现)
乐观锁:认为什么时候都不会出现问题,所以不会上锁。更新数据的时候去判断一下在此期间是否有其他线程修改过这个数据。首先要获取version,然后在更新的时候比较version。
使用 watch key 监控某个数据,相当于乐观锁加锁。
下面先来一个单线程正常的案例:
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> clear
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> watch money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby money 20
QUEUED
127.0.0.1:6379(TX)> incrby out 20
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 80
2) (integer) 20
可见上述代码中,虽然对money进行了watch ,但由于没有其他线程对money进行修改,所以该事务在更新money的时候,其当前版本号与期望版本号一致,所以可以修改成功。
===========================================================
下面模拟两个线程,另一个线程修改值的情况,此时使用watch 起到了乐观锁的作用:
线程1:
127.0.0.1:6379> watch money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby money 20
QUEUED
127.0.0.1:6379(TX)> incrby out 20
QUEUED
127.0.0.1:6379(TX)>
此时线程2突然插队,改掉了money的值:
127.0.0.1:6379> incrby money 1000
(integer) 1080
此时回到线程1,执行事务:
127.0.0.1:6379(TX)> exec
(nil)
127.0.0.1:6379> get money
"1080"
127.0.0.1:6379> get out
"20"
此时由于在exec执行之前,线程2把money的值改掉了,由于money在线程1中受到了watch ,因此此时事务执行失败。此时再去得到money和out的值,money为线程2修改后的,而out为初始的(这里为20是因为上面一个程序加了20,并不是这个事务操作的)。
如果想要通过线程1对money进行修改,要用unwatch 解锁,来获取最新值,然后再加锁进行事务操作。
127.0.0.1:6379> unwatch
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby money 20
QUEUED
127.0.0.1:6379(TX)> incrby out 20
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 1060
2) (integer) 40
值得注意的是,每次提交执行exec后都会自动释放锁,不管是否成功。
|