【死磕Redis系列】四、Redis实现分布式锁

------------------------------------------------------------------------------------------------------慢慢来,一切都来得及


 

前言

分布式应用中经常会遇到并发问题,比如商品减库存操作,需要先读库存,然后再写库存。如果同时进行,就会出现并发问题,这是因为读和写不是在一个原子性操作的,这时就要采用分布式锁来控制了。

 

分布式锁的特点

为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

1、互斥性:任意时刻,只能有一个客户端获取锁,不能同时有两个客户端获取到锁。

2、安全性:锁只能被持有该锁的客户端删除,不能由其它客户端删除。

3、不发生死锁:即使获取锁的客户端因为某些原因(如down机等)而未能释放锁,也要保证其它客户端能获取到锁。

4、容错:当部分节点(redis节点等)down机时,客户端仍然能够获取锁和释放锁。

分布式锁实现方案

  • 基于数据库实现
    • 基于数据库实现分布式锁,主要使用InnoDB下的for update(如使用行级锁,需加唯一索引)
  • 基于Zookeeper实现
    • 在指定节点的目录下,创建一个唯一的瞬时有序节点。可以使用Curator去实现。
  • 基于缓存实现(redis)
    • 主要使用set(setnx用法有缺陷且过时)

从2.6.12版本后, 就可以使用set来获取锁, Lua 脚本来释放锁。set命令nx,xx等参数, 是为了实现 setnx 的功能。

 加锁

public class RedisTool {

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {

        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }

}

String set(String key, String value, String nxxx, String expx, long time);
该方法是: 存储数据到缓存中,并制定过期时间和当Key存在时是否覆盖。

nxxx: 只能取NX或者XX,如果取NX,则只有当key不存在是才进行set,如果取XX,则只有当key已经存在时才进行set
expx: 只能取EX或者PX,代表数据过期时间的单位,EX代表秒,PX代表毫秒。
time: 过期时间,单位是expx所代表的单位。

解锁

public class RedisTool {

    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {

        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }

}

 除此之外,也可以使用Redission(Redis 的客户端)集成进来实现分布式锁

时问题

Redis 的分布式锁不能解决超时问题,如果在加锁和释放锁之间的逻辑执行的太长,以至于超出了锁的超时限制,就会出现问题。因为这时候锁过期了,第二个线程重新持有了这把锁, 但是紧接着第一个线程执行完了业务逻辑,就把锁给释放了,第三个线程就会在第二个线程逻辑执行完之间拿到了锁。 为了避免这个问题,Redis 分布式锁不要用于较长时间的任务。如果真的偶尔出现了,数据出现的小波错乱可能需要人工介入解决。

有一个更加安全的方案是为 set 指令的 value 参数设置为一个随机数,释放锁时先匹配 随机数是否一致,然后再删除 key。但是匹配 value 和删除 key 不是一个原子操作,Redis 也 没有提供类似于 delifequals 这样的指令,这就需要使用 Lua 脚本来处理了,因为 Lua 脚本可以保证连续多个指令的原子性执行(上面的解锁即用的Lua脚本)。

发布了22 篇原创文章 · 获赞 10 · 访问量 6125

猜你喜欢

转载自blog.csdn.net/qq_28681387/article/details/105485584