27.Redis的原子操作
使用Redis的时候,可能会遇到并发操作,对于并发操作,我们将分别说常见的两种解决方案
一种是对Redis的锁操作,
一种是Redis的原子操作
我们这一次说的是Redis的原子操作,Redis中的原子操作可以利用两种实现
一种是Redis提供的原生原子操作
一种是将多个操作写到一个Lua脚本中,以原子性方式执行单个Lua脚本
首先是原生的原子操作
对于一些数据的增删改,最好能做到读取和修改放在一个操作里面,Redis提供了诸如INCR/DECR命令,将三个操作转换为一个原子操作,进行对应的增值和减值的操作,因为其本身就是个单原子操作,所以执行的时候本身就具有互斥性
然后是Lua脚本,Redis会将执行Lua脚本作为一个整体执行,因为Redis的单线程模型,具有原子性,但是因为Redis的单线程模型,导致执行的时候会降低Redis的并发性能,所以最好只将有并发控制的操作放在脚本中
对于Redis中的Lua脚本,可以使用EVAL命令执行
比如,我们由一个限流需求,要求每分钟访问次数不能超过20,第一次访问的时候,需要在value初始化后,设置过期时间,简单的代码如下
//获取ip对应的访问次数
current = GET(ip) //如果超过访问次数超过20次,则报错 IF current != NULL AND current > 20 THEN ERROR “exceed 20 accesses per second” ELSE //如果访问次数不足20次,增加一次访问计数 value = INCR(ip) //如果是第一次访问,将键值对的过期时间设置为60s后 IF value == 1 THEN EXPIRE(ip,60) END //执行其他操作 DO THINGS END |
即根据ip获取次数,然后增加一个值,如果第一次访问,则设置过期时间
虽然是INCR是原子操作,但是如果value是一个全局变量
那么会导致并发情况下,可能出现value无法为1的情况,以致于无法过期,导致用户达到20次访问后一直锁定
上面是使用INCR实现的版本,那么Lua实现版本如下
local current
current = redis.call(“incr”,KEYS[1]) if tonumber(current) == 1 then redis.call(“expire”,KEYS[1],60) end |
对应的执行命令如下
redis-cli –eval lua.script keys,args
这样,就是原子性的执行了,及时是多线程的发送执行命令,同一时间也只有一个脚本执行
小结一下,本章我们主要讲解了单原子操作适用范围,分别说明了单命令操作和Lua脚本,
分别说了两者的局限性
最后一个小问题,Lua脚本中,有必要将读取ip访问数,也就是GET(ip)以及判断访问次数是否超过20加进去?
上面已经说了,需要避免将不需要做并发控制的操作写入脚本中
所以不应该放进去
而且在上面incr的代码中,很明显,value是一个线程内的变量就行,设置为全局变量是一个编程上的纰漏