9

分布式锁原来实现起来这么简单

 3 years ago
source link: http://www.justdojava.com/2021/06/04/Redis/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

分布式锁原来实现起来这么简单

发表于 2021-06-04

| 分类于 Java

阿粉最近迷上了 Redis,为什么呢?感觉 Redis 确实功能很强大呀,一个基于内存的 Key-Value 存储的数据库,竟然有这么多的功能,而阿粉也要实实在在的把 Redis 来弄一下,毕竟面试的时候,Redis 可以说是一个非常不错的加分项。

<–more–>

为什么需要分布式锁?

目前很多的大型项目全部都是基于分布式的,而分布式场景中的数据一致性问题一直是一个不可忽视的问题,大家知道关于分布式的 CAP 理论么?

CAP 理论就是说任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。

而我们的系统最终满足的永远都是最终一致性,而这种最终一致性,有些时候有人会喜欢问关于分布式事务,而有些人则偏重在分布式锁上。

分布式锁的种类

  1. 数据库实现分布式锁

  2. 缓存实现分布式锁

  3. Zookeeper实现分布式锁

但是阿粉选择的就是使用缓存来实现分布式锁,也就是我们在项目中最经常使用的 Redis ,谈到 Redis,那真是可以用在太多地方了,比如说:

  • 发布,订阅消息

  • 商品列表,评论列表

我们今天就来实现用 Redis 来实现分布式锁,并且要学会怎么使用。

1.准备使用 Jedis 的 jar 包,在项目中导入 jar 包。

<!--jedis-->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>
  1. 直接来写个工具类吧
public class RedisPoolUtil {

    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";

    private RedisPoolUtil(){}
    /**
     * 
     * @param jedis 
     * @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;
        }else{
            try{
                Thread.sleep(10);//休眠100毫秒
            }catch(Exception e){
                e.printStackTrace();
            }
        }
        return false;
    }
}

jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); 这个加锁的姿势才是我们最需要了解的,不然你用的时候都不知道怎么使用。

key:加锁的键,实际上就是相当于一个唯一的标志位,不同的业务,你可以使用不同的标志位进行加锁。

requestId:这个东西实际上就是用来标识他是哪一个请求进行的加锁,因为在分布式锁中,我们要知道一件事,就是加锁的和解锁的,必须是同一个客户端才可以。

而且还有一种比较经典的就是 B 把 A 的锁给释放了,导致释放混乱,如果你不加相同的请求,A 线程处理业务,执行了加锁,锁的过期时间是5s, B线程尝试获取锁,如果 A 处理业务时间超过5s,这时候 A 就要开始释放锁,而B在这时候没有检测到这个锁,从而进行了加锁,这时候加锁的时候,A还没处理完对应业务,当他处理完了之后,再释放锁的话,要是就是直接把 B 刚加的锁释放了,要么就是压根都没办法释放锁。

SET_IF_NOT_EXIST:看字面意思,如果 key 不存在,我们进行Set操作,如果存在,啥都不干,也就不在进行加锁。

SET_WITH_EXPIRE_TIME:是否过期

expireTime:这是给 key 设置一个过期的时间,万一你这业务一直被锁着了,然后之后的业务想加锁,你直接给一直持有这个这个锁,不进行过期之后的释放,那岂不是要凉了。

上面的方法中 tryGetDistributedLock 这个方法也就是我们通常使用的加锁的方法。

 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 ("OK".equals(result)) {
            return true;
        }
        return false;

    }

大家看到这个 script的时候,会感觉有点奇怪,实际上他就是一个 Lua 的脚本,而 Lua 脚本的意思也比较简单。

  1. 先获取锁对应的value值,检查是否与requestId相等

  2. 如果相等则删除锁(解锁)

  3. 执行eval()方法

其实这时候就有些人说,直接 del 删除不行么?你试试你如果这么写的话,你们的领导会不会把你的腿给你打断。

这种不先判断锁的拥有者而直接解锁的方式,会导致任何客户端都可以随时进行解锁,也就是说,这锁就算不是我加的,我都能开,这怎么能行呢?

1.jpg

在这里给大家放一段使用的代码,比较简单,但是可以直接用到你们的项目当中

try{
Boolean result = RedisPoolUtil.tryGetDistributedLock(jedis, "xxxxx", uuid, 5000);

if(result) {
        xxxx代码片段
}else{

}

}catch(){

}finally{
RedisPoolUtil.releaseDistributedLock(jedis,"xxxxx", uuid);
}

分布式锁的要求

  1. 满足互斥性。也就是说不管在什么时候,只有一个客户端能够持有锁,不能是多个客户端。

  2. 不能出现死锁。就是说,如果要实现分布式锁,不能说当一个锁没有释放的时候,其他的客户端不能进行加锁,要保证不影响其他的客户端加锁。

  3. 加锁和解锁必须是同一个客户端

分布式的CAP理论

我们先把这个实现方式实现了,然后我们再来说说大家最不愿意看的理论知识,毕竟这理论知识是你面试的时候经常会被问到的。

分布式CAP理论:

加州大学伯克利分校的 Eric Brewer 教授在 ACM PODC 会议上提出 CAP 猜想。2年后,麻省理工学院的 Seth Gilbert 和 Nancy Lynch 从理论上证明了 CAP。之后,CAP 理论正式成为分布式计算领域的公认定理。

也就是说,在二十年前的时候,CAP 理论只是个猜想。结果两年之后被证实了,于是,大家在考虑分布式的时候,就有根据来想了,不再是空想了。

什么是分布式的 CAP 理论 ?

一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项

2.jpg

这个和(Atomicity)不太一样,因为之前看有些人说,在 CAP 理论中的 A 和数据库事务中的 A 是一样的,单词都不一样,那能一样么?

Availability :分布式中的 A 表示的是可用性,也就是说服务一直可用,而且是正常响应时间。

而你在搭建分布式系统的时候,要保证每个节点都是稳定的,不然你的可用性就没有得到相对应的保证,也谈不上是什么分布式了。只能称之为一个伪分布式。

Consistency: 一致性

也就是说你的更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致,这个如果你在使用 Redis 做数据展示的时候,很多面试官都会问你,那你们是怎么保证数据库和缓存的一致性的呢?

毕竟你只是读取的话,没什么问题,但是设计到更新的时候,不管是先写数据库,再删除缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。

所以如果你对这个很感兴趣,可以研究一下,比如说:

  1. 延时双删策略

  2. 懒加载 懒加载可采取双删+TTL失效来实现

如果你能在面试的时候把这些都给面试官说清楚,至少感觉你应该能达到你自己的工资要求。

Partition tolerance:分区容错性

分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务。

其实在 CAP 理论当中,我们是没有办法同时满足一致性、可用性和分区容错性这三个特性,所以有所取舍就可以了。

关于使用 Redis 分布式锁,大家学会了么?


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK