分布式锁——Redis实现

分布式锁是在分布式场景中,实现共享资源互斥访问的一种方式。Java中synchronized或ReentrantLock只能保证在一个jvm中的最多只有一个线程可以获取资源的锁,但是如果是分布式场景,会有多个jvm中各自的线程都会竞争共享资源。这时synchronized或ReentrantLock就无能为力,就需要使用分布式锁。

分布式锁的特点

  • 互斥性:同一时间只能有一个线程获取锁
  • 可重入性:一个线程获取锁之后,可以再次获取锁
  • 锁超时:支持锁超时,防止死锁

分布式锁的实现方式

  • redis
  • zookeeper
  • 数据库

本文使用第一种redis实现方式。

用Redis实现分布式锁

网上流传的一种用redis实现分布式锁的方法,直接使用setnx和expire两条命令,其实是错误,因为这两条命令的执行不是原子的,所以无法保证setnx加锁后,expire设置锁超时时间一定成功,这种情况就会出现死锁。

一、使用set命令实现

从redis 2.6.12版本起,set命令增加了一种支持多个参数的格式

set key value [EX seconds] [PX miliseconds] [NX|XX]

EX: 以秒为单位的过期时间

PX:以毫秒为单位的过期时间

NX:if not exists的缩写,表示当key不存在时,才设置值

XX:if exists的缩写,表示当key存在时,才设置值

public class DistributedLock{

    private static final String SET_IF_NOT_EXISTS = "NX";

    private static final String EXPIRE = "EX";
    
    private static final String LOCK_SUCCESS = "OK";

    //返回true表示获取锁成功
    public boolean lock(String key, String sessionId, int expiredSeconds){
        return LOCK_SUCCESS.equals(jedis.set(key, sessionId, SET_IF_NOT_EXISTS, EXPIRE, expiredSeconds);
    }

    //返回true表示释放成功
    public boolean releaseLock(String key, String value){
        String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1]) then return redis.call('del',KEYS[1]) else return 0 end";
        return jedis.eval(luaScript, Collections.singletonList(key), Collections.singletonList(value)).equals(1L));
    }
}

这里的key一般是资源的名称,sessionId要求在分布式场景中是唯一的,比如会话id,可以使用uuid实现。因为在释放锁时会比较value的值,保证不会错误地释放其他锁。释放锁使用lua脚本,保证原子性。

这种方式适用於单节点redis的情况。如果是redis集群,比如说A客户端在Redis的master节点上拿到了锁,但是这个加锁的key还没有同步到slave节点,master故障,发生故障转移,一个slave节点升级为master节点,B客户端也可以获取同个key的锁,但客户端A也已经拿到锁了,这就导致多个客户端都拿到锁。这种情况需要其他方式处理。

二、使用lua脚本实现

使用lua脚本加锁可以保证原子性,释放锁的方法与上面相同

public boolean getLockWithLua(String key, String uniqueId, int seconds) {
    String luaScript = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end";
    List<String> keys = new ArrayList<>();
    List<String> values = new ArrayList<>();
    keys.add(key);
    values.add(UniqueId);
    values.add(String.valueOf(seconds));
    Object result = jedis.eval(lua_scripts, keys, values);
    //判断是否成功
    return result.equals(1L);
}

本文参考以下博文,感谢原作者

https://juejin.im/post/5cc165816fb9a03202221dd5

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章