Redisson实现redis分布式锁

本文章示例代码:https://github.com/yihec/redis_lock.git

1.什么是分布式锁

     :一种用来解决多个执行线程 访问共享资源 错误或数据不一致问题的工具
    分布式锁:解决不同的进程必须以互斥的方式使用共享资源进行操作时,为了防止分布式系统中的多个进程之间相互干扰,我们需要一种分布式协调技术来对这些进程进行调度

  常见的几种分布式锁

  1. 基于 MySQL 中的锁:一种是基于数据库表实现的乐观锁和悲观锁,另一种是基于Mysql自带的悲观锁
  2. 基于 Zookeeper 有序节点:Zookeeper 允许临时创建有序的子节点,这样客户端获取节点列表时,就能够当前子节点列表中的序号判断是否能够获得锁;
  3. .基于 Redis 的单线程:由于 Redis 是单线程,所以命令会以串行的方式执行,并且本身提供了像SETNX(set if not exists)这样的指令,本身具有互斥性;

MySQL 分布式锁
优点 :理解起来简单,不需要维护额外的第三方中间件(比如Redis Zk)。
缺点:   虽然容易理解但是实现起来较为繁琐需要自己考虑锁超时,加事务等等。
性能局限于数据库,一般对比缓存来说性能较低。对于高并发的场景并不是很适合。

ZooKeeper
优点 :ZK可以不需要关心锁超时时间,实现起来有现成的第三方包,比较方便,
并且支持读写锁,ZK获取锁会按照加锁的顺序,所以其是公平锁。对于高可用利用ZK集群进行保证。
缺点:   ZK需要额外维护,增加维护成本,性能和Mysql相差不大,依然比较差。

Redis分布式锁
优点 :对于Redis实现简单,性能对比ZK和Mysql较好。
缺点 :  如果不需要特别复杂的要求,那么自己就可以利用setNx进行实现,对于一些要求比较严格的场景来说的话可以使用RedLock需要维护Redis集群,如果要实现RedLock那么需要维护更多

2.分布式锁的使用场景

  避免不同节点重复相同的工作:比如用户执行了某个操作有可能不同节点会发送多封邮件;
  避免破坏数据的正确性:如果两个节点在同一条数据上同时进行操作,可能会造成数据错误或不一致的情况出现;

3.分布式锁的具体实现

  1.单Redis实例实现分布式锁

    获取锁: SET resource_name my_random_value NX PX 30000   
            这个命令仅在不存在key的时候才能被执行成功(NX选项),并且这个key有一个30秒的自动失效时间(PX属性)。这个key的值是“my_random_value”(一个随机值),这个值在所有的客户端必须是唯一的,所有同一key的获取者                       (竞 争者)这个值都不能一样。value的值必须是随机数主要是为了更安全的释放锁

    释放锁:  if redis.call("get",KEYS[1]) == ARGV[1] then
                  return redis.call("del",KEYS[1])
             else
            return 0
            End

   
    只有key存在并且存储的值和我指定的值一样才能告诉我删除成功。可以通过Lua脚本实现

    

/**
 * redis分布式锁实现
 */
@Slf4j
@Configuration
public class RedisDistributedLock {
    private static final String LOCK_SUCCESS = "OK";
    private static final Long RELEASE_SUCCESS = 1L;

    //只在键不存在时,才对键进行设置操作
    private static final String SET_IF_NOT_EXIST = "NX";
    // 设置键的过期时间为 毫秒
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    /**
     * 分布式锁的键值
     */
    private static final  String lockKey = "redis-lock_";

    /**
     * 锁的超时时间 10s
     */
    private static final  int expireTime = 10000;

    /**
     * 锁等待
     */
    private static final int acquireTimeout  = 1 * 5000;

    @Autowired
    private RedisTemplate redisTemplate;

    public String acquire(String key) {
        RedisConnection conn =  RedisConnectionUtils.getConnection(redisTemplate.getConnectionFactory());
        Jedis jedis = null;
        SetParams setParams = new SetParams(); // 相当于设置一个规范,可应用到其他键值对
        setParams.px(expireTime); // 过期时间100s
        setParams.nx();
        try {
             jedis = (Jedis) conn.getNativeConnection();

            // 获取锁的超时时间,超过这个时间则放弃获取锁
            long end = System.currentTimeMillis() + 50000;
            // 随机生成一个 value
            String requireToken = UUID.randomUUID().toString();
            while (System.currentTimeMillis() < end) {
                String result = jedis.set(lockKey+key, requireToken, setParams);
                if (LOCK_SUCCESS.equals(result)) {
                    log.info("设置redis 锁成功 key = {} value ={}",lockKey+key,requireToken);
                    return requireToken;
                }
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    log.error("线程异常 ex = {}",e);
                    Thread.currentThread().interrupt();
                }
            }
        } catch (Exception e) {
            log.error("获取锁异常 acquire lock due to error", e);
        }finally {
            if(jedis != null){
                jedis.close();
            }
        }

        return null;
    }

    public boolean release(String identify,String key) {
        Jedis jedis = null;
        RedisConnection conn =  RedisConnectionUtils.getConnection(redisTemplate.getConnectionFactory());
        if (identify == null) {
            return false;
        }

        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = new Object();
        try {
            jedis = (Jedis) conn.getNativeConnection();
            result = jedis.eval(script, Collections.singletonList(lockKey+key),
                    Collections.singletonList(identify));
            if (RELEASE_SUCCESS.equals(result)) {
                log.info("释放锁 release lock success, key = {},value:{}",lockKey+key, identify);
                return true;
            }
        } catch (Exception e) {
            log.error("释放锁异常 release lock due to error", e);
        }finally {
            if(jedis != null){
                jedis.close();
            }
        }

        log.info("release lock failed, requestToken:{}, result:{}", identify, result);
        return false;
    }
}

  

  2.集群环境Redis分布式锁的实现

Redlock算法
在Redis的分布式环境中,我们假设有N个Redis master。这些节点完全互相独立,不存在主从复制或者其他集群协调机制。之前我们已经描述了在Redis单实例下怎么安全地获取和释放锁。我们确保将在每(N)个实例上使用此方法获取和释放锁。在这个样例中,我们假设有5个Redis master节点,这是一个比较合理的设置,所以我们需要在5台机器上面或者5台虚拟机上面运行这些实例,这样保证他们不会同时都宕掉。 

为了取到锁,客户端应该执行以下操作:

获取当前Unix时间,以毫秒为单位。
依次尝试从N个实例,使用相同的key和随机值获取锁。在步骤2,当向Redis设置锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试另外一个Redis实例。
客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当且仅当从大多数(这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。
如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。
如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功)。

@Component
@Slf4j
public class RedissonLock {

    @Autowired
    private  RedissonClient redissonClient;


    public void acquire(String lockName) {

        String key = lockName;
        RLock myLock = redissonClient.getLock(key);
        //lock提供带timeout参数,timeout结束强制解锁,防止死锁
        myLock.lock(10, TimeUnit.SECONDS);
        log.info("======lock======key"+lockName);
        log.info("======lock======" + Thread.currentThread().getName());
    }

    public void release(String lockName) {
        String key = lockName;
        RLock myLock = redissonClient.getLock(key);
        myLock.unlock();
        log.info("======unlock======" + lockName);
        log.info("======unlock======" + Thread.currentThread().getName());
    }
}

  

   3.Redisson几种分布式锁

可重入锁:Redisson的分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口,同时还支持自动过期解锁

红锁:Redisson的RedissonRedLock对象实现了Redlock介绍的加锁算法。该对象也可以用来将多个RLock对象关联为一个红锁,每个RLock对象实例可以来自于不同的Redisson实例

读写锁:Redisson的分布式可重入读写锁RReadWriteLock,Java对象实现了java.util.concurrent.locks.ReadWriteLock接口。同时还支持自动过期解锁。该对象允许同时有多个读取锁,但是最多只能有一个写入锁。

公平锁:Redisson分布式可重入公平锁也是实java.util.concurrent.locks.Lock接口的一种RLock对象。在提供了自动过期解锁功能的同时,保证了当多个Redisson客户端线程同时请求加锁时,优先分配给先发出请求的线程。

 4.小结

1.对于分布式锁的要求

1.互斥性:分布式锁需要保证在不同节点的不同线程的互斥。这是最根本的。
2.可重入性:同一个节点上的同一个线程如果获取了锁之后也可以再次获取这个锁。锁超时:和本地锁一样支持锁超时,防止死锁。
3.高可用:加锁和解锁需要高效,同时也需要保证高可用防止分布式锁失效,可以增加降级。
4.支持阻塞和非阻塞:和 ReentrantLock 一样支持 lock 和 trylock 以及 tryLock(long timeOut)。支持公平锁和非公平锁(可选):公平锁的意思是按照请求加锁的顺序获得锁,非公平锁就相反是无序的。这个一般来说实现的比较少。
5.支持公平锁和非公平锁(可选):公平锁的意思是按照请求加锁的顺序获得锁,非公平锁就相反是无序的。这个一般来说实现的比较少。

2.对于分布式锁Redlock是否可靠

Redlock 实在不是一个好的选择,对于需求性能的分布式锁应用它太重了且成本高;对于需求正确性的应用来说它不够安全。因为它对高危的时钟或者说其他上述列举的情况进行了不可靠的假设,如果你的应用只需要高性能的分布式锁不要求多高的正确性,那么单节点 Redis 够了;如果你的应用想要保住正确性,那么不建议 Redlock,建议使用一个合适的一致性协调系统,例如 Zookeeper,且保证存在 fencing token

 文章参考:

https://www.jianshu.com/p/47fd7f86c848

https://redis.io/topics/distlock

posted @ 2020-09-07 11:21  也许明天1  阅读(388)  评论(1编辑  收藏  举报