redis原子操作(单命令与lua脚本)
原子操作是指执行过程不需要加锁并且保证多个操作是原子性的,使用原子操作可以保证并发时数据准确性,降低对系统性能的影响。比如记录投票数分为3步,先读取原投票数,然后将原投票数加1,最后写回redis。如果不使用原子操作并发情况下会造成投票丢失等问题。加锁的话会降低系统性能,而且加锁就不多说了,只能说做的多错的多,能不加锁就不加锁。redis提供以下两种原子操作方法。单命令操作由于redis使用单线程
原子操作是指执行过程不需要加锁并且保证多个操作是原子性的,使用原子操作可以保证并发时数据准确性,降低对系统性能的影响。比如记录投票数分为3步,先读取原投票数,然后将原投票数加1,最后写回redis。如果不使用原子操作并发情况下会造成投票丢失等问题。加锁的话会降低系统性能,而且加锁就不多说了,只能说做的多错的多,能不加锁就不加锁。
redis提供以下两种原子操作方法。
单命令操作
由于redis使用单线程来串行处理客户端的请求操作,所以当 redis 执行某个命令操作时,其他命令无法执行, 所以使用incr,incrby/decr,decrby对数据增加/减少可以避免并发情况下数据错乱问题。以刚才的投票为例
对id为1的记录进行投票
set vote:1 0
使用自增,不需要读取后再进行计算
127.0.0.1:6379> incr vote:article:1
(integer) 1
vip用户投两票
127.0.0.1:6379> incrby vote:article:1 2
(integer) 3
lua脚本
lua是c写的脚本语言,不适合独立开发应用。redis2.6后提供对lua支持,使用lua脚本可以将脚本内命令一次执行保证原子性。
redis调用lua比较简单,可以在lua脚本中使用redis.call或者redis.pcall调用,两者区别在于使用call()调用在遇到错误时只会向上抛出异常,使用pcall()调用在遇到错误时捕获异常。
以刚才投票为例,增加个需求同一用户1分钟能只能投票一次,使用redis单命令是做不到的,下面演示下不同方式执行lua脚本实现该需求
一、redis内直接写lua脚本
简单解释下下面命令,整个命令可分为四部分
一、eval 为执行脚本指令
二、"" 内为脚本内容
三、2 表示有2个key
四、user:1000:article:{1} 表示id为1000的用户,{}redis集群环境下需要使用hash tag将数据请求对应到一个hash slot中,1对应投票文章id。 vote:article:{1} 表示投票id
该脚本判断userid为1000的用户在一分钟内是否对id为1的文章进行过投票,没投过则将投票数加1,否则直接返回
eval "if redis.call('EXISTS',KEYS[1]) == 0 then redis.call('SETEX',KEYS[1],60,1) redis.call('INCR',KEYS[2]) return 200 else return 500 end" 2 user:1000:article:{1} vote:article:{1}
由于redis内以回车表示执行,上面的代码不太好看,我格式化下
eval "
if redis.call('EXISTS',KEYS[1]) == 0 then
redis.call('SETEX',KEYS[1],60,1)
redis.call('INCR',KEYS[2])
return 200
else
return 500
end
"2 user:1000:article:{1} vote:article:{1}
二、redis-cli执行
vim vote.lua输入以下脚本内容保存
-- 脚本内容与redis内直接执行一致,只不过看着有层次感
if redis.call('EXISTS',KEYS[1]) == 0 then
redis.call('SETEX',KEYS[1],60,1)
redis.call('INCR',KEYS[2])
return '200'
else
return '500'
end
使用redis-cli执行
-c表示集群模式,不使用-c则需要使用-p指定{1}中1对应的端口号
redis-cli -a 123456 -c --eval /data/vote.lua user:1000:article:{1} vote:article:{1}
三、将脚本加载到redis执行
将脚本加载到redis可以避免脚本传输的网络开销。
使用script load将脚本加载到redis中,得到唯一值
script load "if redis.call('EXISTS',KEYS[1]) == 0 then redis.call('SETEX',KEYS[1],60,1) redis.call('INCR',KEYS[2]) return 200 else return 500 end"
"9f3faa1435148eed2300863eb67e6abb6e04ad6f"
使用evalsha执行该脚本
evalsha 9f3faa1435148eed2300863eb67e6abb6e04ad6f 2 user:1000:article:{1} vote:article:{1}
刚才以每次投票加1举例,假如vip用户每次可以对一个文章投两票或者三票则需要改写为以下脚本。
eval "if redis.pcall('EXISTS',KEYS[1]) == 0 then redis.call('SETEX',KEYS[1],60,1) redis.call('SET',KEYS[2],redis.call('GET',KEYS[2])+ARGV[1]) return 200 else return 500 end" 2 user:1000:article:{1} vote:article:{1} 2
格式化一下,与递增相比将incr改为redis.call('SET',KEYS[2],redis.call('GET',KEYS[2]) + ARGV[1]) ,通过传value值将其与原value进行相加。
eval "
if redis.pcall('EXISTS',KEYS[1]) == 0 then
redis.call('SETEX',KEYS[1],60,1)
redis.call('SET',KEYS[2],redis.call('GET',KEYS[2]) + ARGV[1])
return 200
else
return 500
end
" 2 user:1000:article:{1} vote:article:{1} 3
更多推荐
所有评论(0)