原子操作是指执行过程不需要加锁并且保证多个操作是原子性的,使用原子操作可以保证并发时数据准确性,降低对系统性能的影响。比如记录投票数分为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
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐