【Redis】Redis事务

一、Redis事务的概念

1.1、事务本质

Redis 事务的本质是一组命令的集合。 事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。

总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系命令。

1.2、Redis事务没有隔离级别的概念

批量操作在发送 EXEC 命令前被放入队列缓存,并不会被实际执行,也就不存在事务内的查询要看到事务里的更新,事务外查询不能看到。

1.3、Redis不保证原子性

Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。 事务中任意命令执行失败,其余的命令仍会被执行。

1.4、Redis事务的三个阶段

  • 开始事务
  • 命令入队
  • 执行事务

1.5、Redis事务相关命令

命令格式作用返回结果
WATCHWATCH key [key …]将给出的Keys标记为监测态,作为事务执行的条件always OK
UNWATCHUNWATCH清除事务中Keys的 监测态,如果调用了 EXEC or DISCARD,则没有必要再手动调用UNWATCHalways OK
MULTIMULTI显式开启redis事务,后续commands将排队,等候使用 EXEC 进行原子执行always OK
EXECEXEC执行事务中的commands队列,恢复连接状态。如果 WATCH 在之前被调用,只有监测中的Keys没有被修改,命令才会被执行,否则停止执行(详见下文,CAS机制)成功: 返回数组,每个元素对应着原子事务中一个 command的返回结果;
失败: 返回NULL(Ruby 返回nil);
DISCARDDISCARD清除事务中的commands队列,恢复连接状态。如果 WATCH 在之前被调用,释放 监测中的Keysalways OK

二、案例

2.1、正常执行

在这里插入图片描述

2.2、放弃事务

在这里插入图片描述

2.3、命令行错误

若在事务队列中存在命令性错误(类似于java编译性错误),则执行EXEC命令时,所有命令都不会执行
在这里插入图片描述

2.4、运行时错误

若在事务队列中存在语法性错误(类似于java的1/0的运行时异常),则执行EXEC命令时,其他正确命令会被执行,错误命令抛出异常。
在这里插入图片描述

2.5、使用watch

案例一:使用watch检测balance,事务期间balance数据未变动,事务执行成功
在这里插入图片描述
案例二:使用watch检测balance,在开启事务后(标注1处),在新窗口执行标注2中的操作,更改balance的值,模拟其他客户端在事务执行期间更改watch监控的数据,然后再执行标注1后命令,执行EXEC后,事务未成功执行。
在这里插入图片描述
一但执行 EXEC 开启事务的执行后,无论事务使用执行成功, WARCH 对变量的监控都将被取消。
故当事务执行失败后,需重新执行WATCH命令对变量进行监控,并开启新的事务进行操作。

总结:
watch指令类似于乐观锁,在事务提交时,如果watch监控的多个KEY中任何KEY的值已经被其他客户端更改,则使用EXEC执行事务时,事务队列将不会被执行,同时返回Null。multi-bulk应答以通知调用者事务执行失败

三、应用场景

lua脚本+redis事务,其使用方法非常简单,例如:

eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second

其中eval是lua脚本的解释器;
eval的第一个参数是脚本的内容,第二个参数是脚本里面KEYS数组的长度(不包括ARGV参数的个数),这里是两个;紧接着就会有两个参数,用于传递个KEYS数组;后面剩下的参数全部传递给ARGV数组,相当于命令行参数。

如果我们想在lua脚本中调用redis的命令该如何操作?
可以在脚本中使用 redis.call() 或 redis.pcall() 直接调用,两者用法类似,只是在遇到错误时,返回错误的提示方式不同。例如:

eval "return redis.call('set',KEYS[1],'bar')" 1 foo

redis确保正一条script脚本执行期间,其它任何脚本或者命令都无法执行。正是由于这种特性,script才可以替代 MULTI/EXEC 作为事务使用。当然,官方文档也说了,正是由于script执行的特性,所以我们不要在script中执行过长开销的程序,否则会验证影响其它请求的执行。

另外,redis为了减少每次客户端发送来的数据带宽(如果script太长,则发送来的内容可能非常多),会把每次新出现的脚本的sha1摘要保存下来,这样后续如果script不变的话,只需要调用evalsha命令+script摘要即可,而不需要重复传递过长的脚本内容。例如:

127.0.0.1:6379> set foo bar
OK
127.0.0.1:6379> eval "return redis.call('get','foo')" 0
"bar"
127.0.0.1:6379> evalsha 6b1bf486c81ceb7edf3c093f4c48582e38c0e791 0
"bar"

从这里可以看出把key和arg以参数的形式传递而不是直接写在script中的好处,因为这样可以把变量提取出来,使得script的sha1摘要保持不变,提高命中率。在应用程序中,可以先使用evalsha进行调用,如果失败,再使用eval进行操作,这样可以在一定程度上提高效率。

有了上面的知识,我们就可以使用lua脚本来灵活的使用redis的事务,这里举几个简单的例子。

3.1、场景一

我们要判断一个IP是不是第一次访问,如果是第一次访问,那么返回状态1,否则插入该ip,并返回状态0.

127.0.0.1:6379> eval "if redis.call('get',KEYS[1]) then return 1 else redis.call('set', KEYS[1], 'test') return 0 end" 1 test_127.0.0.1
(integer) 0
127.0.0.1:6379> eval "if redis.call('get',KEYS[1]) then return 1 else redis.call('set', KEYS[1], 'test') return 0 end" 1 test_127.0.0.1
(integer) 1

3.2、场景二

使用redis限制30分钟内一个IP只允许访问5次

思路:每次想把当前的时间插入到redis的list中,然后判断list长度是否达到5次,如果大于5次,那么取出队首的元素,和当前时间进行判断,如果在30分钟之内,则返回-1,其它情况返回1.

eval "redis.call('rpush', KEYS[1],ARGV[1]);if (redis.call('llen',KEYS[1]) >tonumber(ARGV[2])) then if tonumber(ARGV[1])-redis.call('lpop', KEYS[1])<tonumber(ARGV[3]) then return -1 else return 1 end else return 1 end" 1 'test_127.0.0.1' 1451460590 5 1800

通过上面两个场景可以看到,我们仅仅使用了lua的if语句,就可以实现这么方便的操作,如果使用其它的lua语法,肯定更加方便。如果不用redis事务的话,比如使用Java实现上面的案例,那么我们需要多次访问redis,这样显然性能不高;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值