Redis之RedLock算法以及底层原理

自研redis分布式锁存在的问题以及面试切入点

lock加锁关键逻辑

unlock解锁的关键逻辑

使用Redis的分布式锁

之前手写的redis分布式锁有什么缺点??


Redis之父的RedLock算法

Redis也提供了Redlock算法,用来实现基于多个实例的分布式锁。

锁变量由多个实例维护,即使有实例发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作。

Redlock算法是实现高可靠分布式锁的一种有效解决方案,可以在实际开发中使用

官网
Redis分布式锁


RedLock的设计理念

该方案也是基于(set 加锁、Lua 脚本解锁)进行改良的,所以redis之父antirez 只描述了差异的地方,大致方案如下。

假设我们有N个Redis主节点,例如 N = 5这些节点是完全独立的,我们不使用复制或任何其他隐式协调系统,

为了取到锁客户端执行以下操作:

该方案为了解决数据不一致的问题,直接舍弃了异步复制只使用 master 节点,同时由于舍弃了 slave,为了保证可用性,引入了 N 个节点,官方建议是 5。

本次教学演示用3台实例来做说明。客户端只有在满足下面的这两个条件时,才能认为是加锁成功。

条件1:客户端从超过半数(大于等于N/2+1)的Redis实例上成功获取到了锁;
条件2:客户端获取锁的总耗时没有超过锁的有效时间。

解决方案与容错公式

RedLock的落地实现Redisson

github地址

java 复制代码
https://github.com/redisson/redisson
java 复制代码
https://redisson.pro/docs/configuration/#cluster-mode

pom文件

java 复制代码
 <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.19.1</version>
        </dependency>

RedissonConfig配置类

java 复制代码
package com.atguigu.redislock.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;


@Configuration
public class RedisConfig
{
    @Bean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory)
    {
        RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(lettuceConnectionFactory);
        //设置key序列化方式string
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //设置value的序列化方式json
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());

        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());

        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }

    @Bean
    public Redisson redisson()
    {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://172.18.8.229:6379").setDatabase(0).setPassword("root");

        return (Redisson) Redisson.create(config);
    }
}

业务方法的改造

java 复制代码
  @Autowired
    private Redisson redisson;

    //V9版本
    public String saleV9(){
        String retMessage="";
        RLock redissonLock = redisson.getLock("redisLock");
        redissonLock.lock();
        try {
            //查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //判断库存是否足够
            Integer inventory =result==null?0: Integer.valueOf(result);
            //扣减库存
            if(inventory>0){
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventory));
                retMessage="成功卖出一个商品,库存剩余:"+inventory;
                System.out.println(retMessage+"\t"+"服务端口号"+port);
                try {
                    TimeUnit.SECONDS.sleep(120);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }else{
                retMessage="商品卖完了";
            }


        }finally {
            redissonLock.unlock();
        }

        return retMessage+"\t"+"服务端口号"+port;
    }

Jemeter压测

这样直接删除锁是有bug的

解决方案

if(redissonLock.isLocked() && redissonLock.isHeldByCurrentThread())

{

redissonLock.unlock();

} }

java 复制代码
 @Autowired
    private Redisson redisson;

    //V9版本
    public String saleV9(){
        String retMessage="";
        RLock redissonLock = redisson.getLock("redisLock");
        redissonLock.lock();
        try {
            //查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //判断库存是否足够
            Integer inventory =result==null?0: Integer.valueOf(result);
            //扣减库存
            if(inventory>0){
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventory));
                retMessage="成功卖出一个商品,库存剩余:"+inventory;
                System.out.println(retMessage+"\t"+"服务端口号"+port);
            }else{
                retMessage="商品卖完了";
            }


        }finally {
            if(redissonLock.isLocked() && redissonLock.isHeldByCurrentThread())
            {
                redissonLock.unlock();
            }        }

        return retMessage+"\t"+"服务端口号"+port;
    }

Redisson源码解析

  • 加锁
  • 可重入
  • 续命
  • 解锁
  • 分析步骤
    Redis分布式锁过期了,但是业务逻辑还没处理完怎么办?(还记得之前说过的缓存续命么)
    守护线程续命

    在获取锁成功后,给锁加一个watch dog,watchdog会启动一个定时任务,在锁没有被释放且快要过期的时候会续期。

源码分析

通过redissson新建出来的锁key,默认是30s

加锁的核心代码

Lua脚本加锁

  • 通过 exists 判断,如果锁不存在,则设置值和过期时间,加锁成功。
  • 通过 hexists 判断,如果锁已存在,并且锁的是当前线程,则证明是重入锁,加锁成功。
  • 如果锁已存在,但锁的不是当前线程,则证明有其他线程持有锁。返回当前锁的过期时间(代表了锁 key 的剩余生存时间),加锁失败。
    看门狗的锁续期


    客户端A加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端A还持有锁key,那么就会不断的延长锁key的生存时间,默认每次续命又从30秒新开始


    Lua脚本执行看门狗的锁续期


    解锁方法


    多机案例
    理论参考




    实战演示:
    docker启动三个redis实例
java 复制代码
server.port=9090
spring.application.name=redlock


spring.swagger2.enabled=true


spring.redis.database=0
spring.redis.password=
spring.redis.timeout=3000
spring.redis.mode=single

spring.redis.pool.conn-timeout=3000
spring.redis.pool.so-timeout=3000
spring.redis.pool.size=10

spring.redis.single.address1=172.18.8.229:6382
spring.redis.single.address2=172.18.8.229:6383
spring.redis.single.address3=172.18.8.229:6384

redis三个实例对应的配置类

java 复制代码
package com.atguigu.redis.redlock.config;

import org.apache.commons.lang3.StringUtils;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

@Configuration
@EnableConfigurationProperties(RedisProperties.class)
public class CacheConfiguration {

    @Autowired
    RedisProperties redisProperties;

    @Bean
    RedissonClient redissonClient1() {
        Config config = new Config();
        String node = redisProperties.getSingle().getAddress1();
        node = node.startsWith("redis://") ? node : "redis://" + node;
        SingleServerConfig serverConfig = config.useSingleServer()
                .setAddress(node)
                .setTimeout(redisProperties.getPool().getConnTimeout())
                .setConnectionPoolSize(redisProperties.getPool().getSize())
                .setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());
        if (StringUtils.isNotBlank(redisProperties.getPassword())) {
            serverConfig.setPassword(redisProperties.getPassword());
        }
        return Redisson.create(config);
    }

    @Bean
    RedissonClient redissonClient2() {
        Config config = new Config();
        String node = redisProperties.getSingle().getAddress2();
        node = node.startsWith("redis://") ? node : "redis://" + node;
        SingleServerConfig serverConfig = config.useSingleServer()
                .setAddress(node)
                .setTimeout(redisProperties.getPool().getConnTimeout())
                .setConnectionPoolSize(redisProperties.getPool().getSize())
                .setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());
        if (StringUtils.isNotBlank(redisProperties.getPassword())) {
            serverConfig.setPassword(redisProperties.getPassword());
        }
        return Redisson.create(config);
    }

    @Bean
    RedissonClient redissonClient3() {
        Config config = new Config();
        String node = redisProperties.getSingle().getAddress3();
        node = node.startsWith("redis://") ? node : "redis://" + node;
        SingleServerConfig serverConfig = config.useSingleServer()
                .setAddress(node)
                .setTimeout(redisProperties.getPool().getConnTimeout())
                .setConnectionPoolSize(redisProperties.getPool().getSize())
                .setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());
        if (StringUtils.isNotBlank(redisProperties.getPassword())) {
            serverConfig.setPassword(redisProperties.getPassword());
        }
        return Redisson.create(config);
    }
}

controller的演示方法

java 复制代码
@RestController
@Slf4j
public class RedLockController
{
    public static final String CACHE_KEY_REDLOCK = "ATGUIGU_REDLOCK";

    @Autowired RedissonClient redissonClient1;
    @Autowired RedissonClient redissonClient2;
    @Autowired RedissonClient redissonClient3;

    @GetMapping(value = "/multilock")
    public String getMultiLock()
    {
        String taskThreadID = Thread.currentThread().getId()+"";

        RLock lock1 = redissonClient1.getLock(CACHE_KEY_REDLOCK);
        RLock lock2 = redissonClient2.getLock(CACHE_KEY_REDLOCK);
        RLock lock3 = redissonClient3.getLock(CACHE_KEY_REDLOCK);

        RedissonMultiLock redLock = new RedissonMultiLock(lock1, lock2, lock3);

        redLock.lock();
        try
        {
            log.info("come in biz multilock:{}",taskThreadID);
            try { TimeUnit.SECONDS.sleep(30); } catch (InterruptedException e) { e.printStackTrace(); }
            log.info("task is over multilock:{}",taskThreadID);
        }catch (Exception e){
            e.printStackTrace();
            log.error("multilock exception:{}",e.getCause()+"\t"+e.getMessage());
        }finally {
            redLock.unlock();
            log.info("释放分布式锁成功key:{}",CACHE_KEY_REDLOCK);
        }
        return "multilock task is over: "+taskThreadID;
    }
}

锁续期成功

宕机后仍然成功

相关推荐
J先生x19 分钟前
【IP101】图像分割技术全解析:从传统算法到深度学习的进阶之路
图像处理·人工智能·深度学习·学习·算法·计算机视觉
NON-JUDGMENTAL22 分钟前
第2章 算法分析基础
java·数据结构·算法
等不到来世33 分钟前
.net在DB First模式使用pgsql
数据库·pgsql·db first
写个博客43 分钟前
代码随想录算法训练营第三十三天(补)
算法
wen__xvn44 分钟前
每日一题洛谷P1025 [NOIP 2001 提高组] 数的划分c++
数据结构·c++·算法
文牧之1 小时前
PostgreSQL 判断索引是否重建过的方法
运维·数据库·postgresql
鱼鱼不愚与1 小时前
处理 Clickhouse 内存溢出
数据库·分布式·clickhouse
gadiaola2 小时前
MySQL从入门到精通(二):Windows和Mac版本MySQL安装教程
数据库·mysql
朱剑君2 小时前
排序算法——桶排序
算法·排序算法
明天不下雨(牛客同名)2 小时前
MySQL关于锁的面试题
数据库·mysql