redis锁

云程序员 2021年03月03日 21次浏览

在Java中,大家都非常熟悉锁。在并发编程中,我们通过锁,来避免由于竞争而造成的数据不一致问题。通常,我们以synchronized 、Lock来使用它。

但是Java中的锁,只能保证在同一个JVM进程内中的安全,分布式系统的锁就需要用到redis、zookeeper、本文只要是使用redis锁来进行讲述

原理

redis上能实现锁的重要原理有两个

  1. redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系。

  2. redis的SETNX命令,当且仅当 key 不存在时将 key 的值设为 value,并返回1;若给定的 key 已经存在,则 SETNX 不做任何动作,并返回0。

上面两个特点使redis的锁实现起来非常简单

设计

所有的锁大概都会经历下面三个过程,redis锁也一样,我们来详细说说redis的加解锁过程

加锁

加锁实际上就是在redis中,给Key键设置一个值,为避免客户端奔溃导致锁不能解除,需要设定一个过期时间,命令如下

SET lock_key value NX PX 5000

值得注意的是:
random_value 是客户端生成的唯一的字符串。
NX 代表只在键不存在时,才对键进行设置操作。
PX 5000 设置键的过期时间为5000毫秒。

这样,如果上面的命令执行成功,则证明客户端获取到了锁。

解锁

解锁的过程就是将Key键删除。但是要保证自己设置的可以、键不能被其他人删除。这时候命令中的value的价值就体现出来。

为了保证解锁操作的原子性,有两种方式

  1. 使用我们用LUA脚本完成这一操作。先判断当前锁的字符串是否与传入的值相等,是的话就删除Key,解锁成功。
  2. 使用watch key的方式
锁超时

在早期版本的redis中,SET NX是不能设置超时时间的,所以要用LUA脚本一起设置超时时间,或者第二个命令设置超时时间,和解锁一样,一旦操作不能原子执行,需要考虑的方面就会多很多了

代码实现
package redis;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.exceptions.JedisException;

import java.util.List;
import java.util.UUID;


public class DistributedLock {
    private final JedisPool jedisPool;

    public DistributedLock(JedisPool jedisPool) {
        this.jedisPool = jedisPool;
    }

    /**
     * 加锁
     * @param locaName  锁的key
     * @param acquireTimeout  获取超时时间
     * @param timeout   锁的超时时间
     * @return 锁标识
     */
    public String lockWithTimeout(String locaName,
                                  long acquireTimeout, long timeout) {
        Jedis conn = null;
        String retIdentifier = null;
        try {
            // 获取连接
            conn = jedisPool.getResource();
            // 随机生成一个value
            String identifier = UUID.randomUUID().toString();
            // 锁名,即key值
            String lockKey = "lock:" + locaName;
            // 超时时间,上锁后超过此时间则自动释放锁
            int lockExpire = (int)(timeout / 1000);

            // 获取锁的超时时间,超过这个时间则放弃获取锁
            long end = System.currentTimeMillis() + acquireTimeout;
            while (System.currentTimeMillis() < end) {
                if (conn.setnx(lockKey, identifier) == 1) {
                    conn.expire(lockKey, lockExpire);
                    // 返回value值,用于释放锁时间确认
                    retIdentifier = identifier;
                    return retIdentifier;
                }
                // 返回-1代表key没有设置超时时间,为key设置一个超时时间,是为了解决别人设置了锁,但没设置超时时间的问题。
                //但这个可能会将别人的key(各种状态),设置了一个新时间
                // 新版本的redis可以一个命令解决了
                if (conn.ttl(lockKey) == -1) {
                    conn.expire(lockKey, lockExpire);
                }

                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        } catch (JedisException e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                conn.close();
            }
        }
        return retIdentifier;
    }

    /**
     * 释放锁
     * @param lockName 锁的key
     * @param identifier    释放锁的标识
     * @return
     */
    public boolean releaseLock(String lockName, String identifier) {
        Jedis conn = null;
        String lockKey = "lock:" + lockName;
        boolean retFlag = false;
        try {
            conn = jedisPool.getResource();
            if (identifier.equals(conn.get(lockKey))){
                 //存在间隙删除别人key的可能性
                  jedis.del(lockKey);
            }
        } catch (JedisException e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                conn.close();
            }
        }
        return retFlag;
    }
}

上述代码存在比较多的问题,具体可以先了解下一个redis锁,大概需要考虑哪些问题

  1. 操作原子性,例如判断是存在、设置值和时间应该是在同一个命令上,判断是否相等并删除也一样。

  2. 能手工删除、遇到问题也能超时删除。

  3. 数据安全性,不能删除不是自己设置的 。

  4. 容错性,不管是客户端的崩溃,还是服务端的崩溃。

一般情况下

  1. 操作原子性一般使用LUA脚本就可以解决。

  2. 安全性,不能删除别人的数据。

  3. 客户端崩溃,需要设置自动过期。

  4. 服务端的容错,集群故障转移是否能完美解决问题?为什么redisson要设计RedLock。

redisson的锁

分布式锁

具体类定义如下,通过类定义,可以知道redisson实现了java的Lock接口,使用上和java锁基本一致,同时提供了异步锁,超时等功能

public class RedissonLock extends RedissonBaseLock {
   ...
}

public abstract class RedissonBaseLock extends RedissonExpirable implements RLock{
    ...
}

public interface RLock extends Lock, RLockAsync{
    ...
}

分布式读写锁

读写锁的要求

读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性。一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。

如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里,直到没有任何写者或读者。

如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁。

Redisson中的读写锁直接继承了RedissonLock,只改写了小部分的实操代码,如加锁逻辑,具体逻辑,有空详细说明

public class RedissonReadLock extends RedissonLock implements RLock {
    ...
}

public class RedissonWriteLock extends RedissonLock implements RLock {
    ...
}
分布式公平锁

所谓公平锁,就是保证客户端获取锁的顺序,跟他们请求获取锁的顺序,是一样的。公平锁需要排队,谁先申请获取这把锁,谁就可以先获取到这把锁,是按照请求的先后顺序来的。

Redisson中的公平锁也是直接继承了RedissonLock,只改写了小部分的实操代码,如加锁逻辑,具体逻辑,有空详细说明

public class RedissonFairLock extends RedissonLock implements RLock {
    ...
}
联合锁

Redisson的RedissonMultiLock对象可以将多个RLock对象关联为一个联锁,每个RLock对象实例可以来自于不同的Redisson实例。

public class RedissonMultiLock implements RLock {
    ...
}
红锁

这个锁的算法实现了多redis实例的情况,对象关联为一个红锁,每个RLock对象实例可以来自于不同的Redisson实例。

相对于单redis节点来说,优点在于

  1. 防止了单节点故障造成整个服务停止运行的情况
  2. 在多节点中锁的设计,及多节点同时崩溃等各种意外情况有自己独特的设计方法

直接使用集群为什么不行?

因为redis在进行主从复制时是异步完成的,比如在clientA获取锁后,主redis复制数据到从redis过程中崩溃了,导致没有复制到从redis中,然后从redis选举出一个升级为主redis,造成新的主redis没有clientA 设置的锁,导致clientB尝试获取锁,并且能够成功获取锁,导致互斥失效

public class RedissonRedLock extends RedissonMultiLock {
    ...
}
事务锁

这个锁,没有什么特别的,就是之前的锁名称都是使用线程id,这个锁,就在线程上面再加一个事务Id

public class RedissonTransactionalLock extends RedissonLock {
    private final String transactionId;
    
    public RedissonTransactionalLock(CommandAsyncExecutor commandExecutor, String name, String transactionId) {
        super(commandExecutor, name);
        this.transactionId = transactionId;
    }
    
    @Override
    protected String getLockName(long threadId) {
        return super.getLockName(threadId) + ":" + transactionId;
    }
}
自旋锁

官方文档显示,这不是一个公平锁,所以不能保证执行顺序。
当客户端丢失了连接,会自动释放锁。
此锁实现不使用发布/订阅机制。它可以在大型Redis集群中使用,尽管目前还很幼稚

public class RedissonSpinLock extends RedissonBaseLock {
    ...
}