文章目录

  • 1.Redis的两种原子操作方法
  • 2.Redis实现分布式锁
  • 2.1.分布式锁的概念
  • 2.2.Redis分布式锁的实现
  • 2.2.1.基于单个redis节点实现分布式锁
  • 2.2.2.基于多个redis节点实现高可靠的分布式锁
  • 2.3.redis加锁过程中的错误使用

java 实现redis原子性操作 redis原子性操作有哪些_学习

使用redis时,碰到并发有两种处理方式

  • 第一种:看是否能够使用原子操作
  • 第二种:分布式锁

为什么需要优先第一种呢,因为我们需要考虑到分布式锁会降低并发度,并且分布式锁需要使用到其余的系统(共享存储系统)来实现

而原子操作则无需加锁,既能控制并发度,也能够减少对系统并发性的影响

1.Redis的两种原子操作方法

  • (1)把多个操作放在redis中实现成一个操作,也就是单命令操作
    例如:INCR/DECRSETNX命令,其将读取-修改-写入合并为了一个操作,而redis本身就是单线程的,命令与命令之间是互斥的,所以该操作符合原子性
  • (2)把多个命令写到lua脚本中,以原子性的方式执行单个脚本
    Redis会把整个Lua脚本作为一个整体执行,在执行的过程中不会被其他命令打断,从而保证了Lua脚本中操作的原子性
    另外使用lua脚本时,还有一些注意点:
    1、lua 脚本尽量只编写通用的逻辑代码,避免直接写死变量。变量通过外部调用方传递进来,这样 lua 脚本的可复用度更高。
    2、建议先使用SCRIPT LOAD命令把 lua 脚本加载到 Redis 中,然后得到一个脚本唯一摘要值,再通过EVALSHA命令 + 脚本摘要值来执行脚本,这样可以避免每次发送脚本内容到 Redis,减少网络开销。

2.Redis实现分布式锁

2.1.分布式锁的概念

分布式锁类似于单机上的锁,通过一个变量来维护,加锁操作实际上就是判断该变量是否为0,释放锁操作实际上就是将该变量置为0。

只是分布式锁的变量变为了一个共享变量,关于分布式锁的要求如下:

  • 互斥性:加锁和释放锁过程涉及多个操作,需要保证操作的原子性,只允许一个请求拿到锁
  • 可靠性:需要保证依赖的共享系统的可靠性,进而保证锁的可靠性
  • 释放锁只能够释放自己加的锁

2.2.Redis分布式锁的实现

2.2.1.基于单个redis节点实现分布式锁

  • 加锁操作:redis通过键值对lock_key:0保存变量,后续其他加锁请求发现该键值对存在,则返回失败标识

加锁过程包含读取、判断变量值、修改锁变量值为1三个过程,redis是如何保证原子性的:

SETNX命令:redis先判断当前key是否存在,如果不存就创建该键值,存在就直接返回,redis的具体实现如下

SET key value [EX seconds | PX milliseconds]  [NX]
  • 释放锁操作:执行完业务流程后,使用DEL删除锁变量,释放锁过程包含了读取锁变量、判断值、删除锁变量多个操作,因此使用Lua脚本,原子性的方式执行保证释放锁操作的原子性
  • 使用SETNX+DEL实现分布式锁存在的问题
  • (1)可能申请了锁但是没有正常的释放,就会导致其他请求无法申请锁
    解决办法:给key设置过期时间,将影响降低
  • (2)删除锁操作没有区分谁申请的锁,就是说A可以释放B的锁
    解决办法:区分当前锁是那个客户端申请的,其实就是给锁的变量设置一个唯一id进行客户端的区分,删除的时候也比较这个值是不是和申请锁的客户端的唯一id相等,相等才能执行删除锁操作

知道了解决方案后,我们可以来看下redis的实现

// 加锁, unique_value作为客户端唯一性的标识
SET lock_key unique_value NX PX 10000

//释放锁 比较unique_value是否相等,避免误释放,这里是使用lua实现
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end
redis-cli  --eval  unlock.script lock_key , unique_value

2.2.2.基于多个redis节点实现高可靠的分布式锁

由于一个redis实例可能出现宕机的风险,因此为了保证可靠性,我们可以采取多个实例实现redis的分布式锁

为了避免Redis实例故障而导致的锁无法工作的问题,Redis的开发者Antirez提出了分布式锁算法Redlock

Redlock

基本思路:让客户端请求多个redis实例依次请求加锁,能够获得大多数(2n+1)实例的锁,则申请锁成功。这样能够有效避免某个实例宕机导致锁操作异常

基本流程:

  • 第一步:客户端获取当前时间
  • 第二步:客户端按顺序向N个实例请求加锁操作。加锁方法仍是使用SETNX这里redis实例给每个客户端进行加锁时,会设置客户端的超时时间,如果在超时时间内没有加锁成功则会请求下一个实例,一般的超时时间为几十ms
  • 第三步:完成了与所有实例的加锁操作之后需要计算整个过程的耗时,用于比较锁的过期时间,如果请求锁的时间大于了过期时间,则将该锁释放,如果客户端请求成功的实例不占大多数,也会释放锁

RedLock算法中,只需要保证N个redis实例的半数以上可用就能保证锁操作的可靠性

2.3.redis加锁过程中的错误使用

  • 加锁操作
    错误一:
Long result = jedis.setnx(lockKey, requestId);
    if (result == 1) {
        // 若在这里程序突然崩溃,则无法设置过期时间,将发生死锁
        jedis.expire(lockKey, expireTime);
    }
  • 解锁操作
// 判断加锁与解锁是不是同一个客户端
    if (requestId.equals(jedis.get(lockKey))) {
        //删除点: 若在此时,这把锁突然不是这个客户端的,则会误解锁
        jedis.del(lockKey);
    }

可能会存在当前代码执行到删除点时,锁正好过期然后A又申请到了锁,此时删除会释放A的锁导致错误