Redis如何实现分布式锁?

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

📢📢📢📣📣📣

哈喽大家好我是【一心同学】一位上进心十足的【Java领域博主】😜😜😜

✨【一心同学】的写作风格喜欢用【通俗易懂】的文笔去讲解每一个知识点而不喜欢用【高大上】的官方陈述。

✨【一心同学】博客的领域是【面向后端技术】的学习未来会持续更新更多的【后端技术】以及【学习心得】。

✨如果有对【后端技术】感兴趣的【小可爱欢迎关注一心同学】💞💞💞

❤️❤️❤️感谢各位大可爱小可爱❤️❤️❤️ 


目录

一、什么是分布式锁

二、分布式锁实现方案

🌴 常见的分布式锁方案

🌴 推演实现分布式锁

🌵 问题发生死锁

🌵 解决——死锁

🌵 解决——锁被别人释放了

🌵 解决——锁的过期时间

三、redisson实现分布式锁

🌴 依赖

🌴 配置

🌴 业务类使用


一、什么是分布式锁

分布式锁可以理解为控制分布式系统有序的去对共享资源进行操作通过互斥来保持一致性。

例如共享的资源就是一个房子里面有各种书分布式系统就是要进屋看书的人 分布式锁就是保证这个房子只有一个门并且一次只有一个人可以进而且门只有一把钥匙。 然后许多人要去看书进行排队第一个人拿着钥匙把门打开进屋看书并且把门锁上 然后第二个人没有钥匙那就等着等第一个出来然后你在拿着钥匙进去然后就是以此类推。

单机环境中应用是在同一进程下的只需要保证单进程多线程环境中的线程安全性通过 JAVA 提供的 volatile、ReentrantLock、synchronized 以及 concurrent 并发包下一些线程安全的类等就可以做到。而在多机部署环境中不同机器不同进程就需要在多进程下保证线程的安全性了。因此分布式锁应运而生。

 

二、分布式锁实现方案

🌴 常见的分布式锁方案

分类方案实现原理优点缺点
基于数据库基于mysql 表唯一索引1.表增加唯一索引
2.加锁执行insert语句若报错则表明加锁失败
3.解锁执行delete语句
完全利用DB现有能力实现简单1.锁无超时自动失效机制有死锁风险
2.不支持锁重入不支持阻塞等待
3.操作数据库开销大性能不高
基于MongoDB findAndModify原子操作1.加锁执行findAndModify原子命令查找document若不存在则新增
2.解锁删除document
实现也很容易较基于MySQL唯一索引的方案性能要好很多1.大部分公司数据库用MySQL可能缺乏相应的MongoDB运维、开发人员
2.锁无超时自动失效机制
基于分布式协调系统基于ZooKeeper1.加锁在/lock目录下创建临时有序节点判断创建的节点序号是否最小。若是则表示获取到锁否则则watch /lock目录下序号比自身小的前一个节点
2.解锁删除节点
1.由zk保障系统高可用
2.Curator框架已原生支持系列分布式锁命令使用简单
需单独维护一套zk集群维保成本高
基于缓存基于redis命令1. 加锁执行setnx若成功再执行expire添加过期时间
2. 解锁执行delete命令
实现简单相比数据库和分布式系统的实现该方案最轻性能最好1.setnx和expire分2步执行非原子操作若setnx执行成功但expire执行失败就可能出现死锁
2.delete命令存在误删除非当前线程持有的锁的可能
3.不支持阻塞等待、不可重入
基于redis Lua脚本能力1. 加锁执行SET lock_name random_value EX seconds NX 命令

2. 解锁执行Lua脚本释放锁时验证random_value 
-- ARGV[1]为random_value,  KEYS[1]为lock_name

if redis.call("get", KEYS[1]) == ARGV[1] then

    return redis.call("del",KEYS[1])

else

    return 0

end

同上实现逻辑上也更严谨除了单点问题生产环境采用用这种方案问题也不大。不支持锁重入不支持阻塞等待

在以上的方案中redis+lua基本可应付工作中分布式锁的需求但是还有一种解决方案——redisson分布式锁相比以上方案redisson保持了简单易用、支持锁重入、支持阻塞等待、Lua脚本原子操作。

🌴 推演实现分布式锁

分布式锁实现条件

1、互斥性。在任意时刻只有一个客户端能持有锁。

2、不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁也能保证后续其他客户端能加锁。

3、解铃还须系铃人。加锁和解锁必须是同一个客户端客户端自己不能把别人加的锁给解了即不能误解锁。

4、具有容错性。只要大多数Redis节点正常运行客户端就能够获取和释放锁。

想要实现分布式锁必须要求Redis有互斥的能力。可以使用SETNX命令其含义是SET IF NOT EXIST即如果key不存在才会设置它的值否则什么也不做。两个客户端进程可以执行这个命令达到互斥就可以实现一个分布式锁。

 

我们可以将整体流程写成以下伪代码

// 加锁
SETNX lock_key 1
// 业务逻辑
DO SOMETHING
// 释放锁
DEL lock_key

🌵 问题发生死锁

不难发现上面的代码在某些场景下是会发生死锁的如

1、程序处理业务逻辑异常没及时释放锁。

2、进程挂了没机会释放锁。

🌵 解决——死锁

为了解决以上死锁问题最容易想到的方案是在申请锁时在Redis中实现时给锁设置一个过期时间假设操作共享资源的时间不会超过10s那么加锁时给这个key设置10s过期即可。

如何加锁

加锁、设置过期时间是2条命令有可能只执行了第一条第二条却执行失败例如

  • SETNX执行成功执行EXPIRE时由于网络问题执行失败
  • SETNX执行成功Redis异常宕机EXPIRE没有机会执行
  • SETNX执行成功客户端异常崩溃EXPIRE没有机会执行

之这两条命令如果不能保证是原子操作就有潜在的风险导致过期时间设置失败依旧有可能发生死锁问题。幸好在Redis 2.6.12之后Redis扩展了SET命令的参数可以在SET的同时指定EXPIRE时间这条操作是原子的例如以下命令是设置锁的过期时间为10秒。

SET lock_key 1 EX 10 NX

新问题

但是过期时间也带来了新的问题如下

 

  • 客户端1加锁成功开始操作共享资源。
  • 客户端1操作共享资源耗时太久超过了锁的过期时间锁失效锁被自动释放。
  • 客户端2加锁成功开始操作共享资源。
  • 客户端1操作共享资源完成在finally块中手动释放锁但此时它释放的是客户端2的锁。

我们把上面归为两个主要问题

1、锁过期

2、释放了别人的锁

第1个问题是评估操作共享资源的时间不准确导致的客户端在拿到锁之后在操作共享资源时遇到的场景是很复杂的既然是预估的时间也只能是大致的计算不可能覆盖所有导致耗时变长的场景。

第2个问题是释放了别人的锁原因在于释放锁的操作是无脑操作并没有检查这把锁的归属这样解锁不严谨。

🌵 解决——锁被别人释放了

客户端在加锁时设置一个只有自己知道的唯一标识进去如可以是自己的线程ID如果是redis实现就是SET key unique_value EX 10 NX。之后在释放锁时要先判断这把锁是否归自己持有只有是自己的才能释放它。

//释放锁 比较unique_value是否相等避免误释放
if redis.get("key") == unique_value then
    return redis.del("key")

但是释放锁使用的是GET + DEL两条命令这时又会遇到原子性问题了。这时候我们就需要Lua脚本帮我们将其转为原子命令因为Redis处理每个请求是单线程执行的在执行一个Lua脚本时其它请求必须等待直到这个Lua脚本处理完成这样一来GET+DEL之间就不会有其他命令执行了。

以下是使用Lua脚本unlock.script实现的释放锁操作的伪代码其中KEYS[1]表示lock_keyARGV[1]是当前客户端的唯一标识这两个值都是我们在执行 Lua脚本时作为参数传入的。

//Lua脚本语言释放锁 比较unique_value是否相等避免误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

最后在redis客户端执行以下命令即可

redis-cli  --eval  unlock.script lock_key , unique_value 

到此我们的流程都是非常严谨的目前流程如下

1、加锁时要设置过期时间SET lock_key unique_value EX expire_time NX

2、操作共享资源

3、释放锁Lua脚本先GET判断锁是否归属自己再DEL释放锁

现在只剩一个问题了确定锁的过期时间。

🌵 解决——锁的过期时间

方案加锁时先设置一个预估的过期时间然后开启一个守护线程定时去检测这个锁的失效时间如果锁快要过期了操作共享资源还未完成那么就自动对锁进行续期重新设置过期时间。

这种方案是比较好的而且有一个库把这些工作都封装好了它就是RedissonRedisson是一个Java语言实现的Redis SDK客户端在使用分布式锁时它就采用了自动续期的方案来避免锁过期这个守护线程我们一般叫它看门狗线程。客户端一旦加锁成功就会启动一个watch dog看门狗线程它是一个后台线程会每隔一段时间这段时间的长度与设置的锁的过期时间有关检查一下如果检查时客户端还持有锁key也就是说还在操作共享资源那么就会延长锁key的生存时间。而且使用redisson来实现分布式锁由于其加锁解锁都封装好了思路也是我们一路推导过来的思路所以使用起来非常友好。

三、redisson实现分布式锁

我们先来看看不使用redisson的话加解锁过程是有多复杂

加锁


    //加锁之后返回锁的持有者(锁的value使用唯一时间戳标志每个客户端,保证只有锁的持有者才可以释放锁)
    public static String lock(Jedis jedis, String key,Long waitEnd,String requestId) {
        try {
            // 1秒内数次加锁如果失败,则不断请求重新获取锁,超过1秒还没能加锁,就加锁失败(为了每个线程拥有公平的机会获取锁)
            while (System.currentTimeMillis() < waitEnd) {// 1秒类不断尝试加锁(加锁之后返回锁的持有者)
                String result = jedis.set(key, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, EXPIRE_TIME);
                
                if (LOCK_SUCCESS.equals(result)) {
                    return requestId;
                }
            }
        } catch (Exception ex) {
            log.error("lock error", ex);
        }
        return null;
}

解锁

   
 
    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean unLock(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;
    }

是不是过程十分繁琐现在开始我们的redisson来实现

🌴 依赖

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.16.8</version>
        </dependency>

🌴 配置

package com.yixin.config;

import org.redisson.Redisson;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.IOError;

@Configuration
public class MyRedisConfig {

    @Bean
    public Redisson redisson() throws IOError{
        //1、创建配置
        Config config = new Config();
        //2、根据Config 创建出RedissonClient示例
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        return (Redisson) Redisson.create(config);
    }

}

🌴 业务类使用

package com.yixin.service;

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class AccountService {

    @Autowired
    private Redisson redisson;

    public void submit(){
        String lockKey = "product_order";
        RLock redissionLock=redisson.getLock(lockKey);
        redissionLock.lock(); // 相当于 setIfAbsent(lockKey,clientId,30,TimeUnit.SECONDS);
        //业务操作。。。
        redissionLock.unlock(); //底层是已经封装lua脚本了
    }

}

这样就完成好了看着是不是特别友好。

如果这篇【文章】有帮助到你希望可以给【一心同学】点个👍创作不易相比官方的陈述我更喜欢用【通俗易懂】的文笔去讲解每一个知识点如果有对【后端技术】感兴趣的小可爱也欢迎关注❤️❤️❤️ 【一心同学】❤️❤️❤️我将会给你带来巨大的【收获与惊喜】💕💕

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: redis

“Redis如何实现分布式锁?” 的相关文章