Redlock实现算法和底层源码分析

一、Redlock 红锁算法

1.1.Redlock官网说明

官网地址:https://redis.io/docs/manual/patterns/distributed-locks/,截图如下:

不同语言直接对于redlock 的实现,Java使用Redission来实现

1.2.Redlock解决手写分布式锁的单点故障问题

如果Redis锁所在的机器只有一台,突然宕机出现问题,但是通过集群的方式解决单点故障也是存在问题,说明如下:

上图通过集群的方式解决单点故障存在的问题

  1. 客户A通过redis的set命令建分布式锁成功并持有锁.
  2. 正常情况主从机都有这分布式锁。
  3. 突然出故障了,master还没来得同步数据给slave,此时slave器上没有对应锁的信息。
  4. 从机slave上位,变成了新master,但是slave没有该锁,
  5. 客户B照样可以同样的建锁成功,出现了很严重的问题:一个锁被多次建立多次使用.

CAP里面的CP遭到了破坏,而且redis无论单机、主从、响兵和主从均有这样风险,线程 1 首先获取锁成功,将键值对写入 redis 的 master 节点,在 redis 将该键值对同步到 slave 节点之前,master 发生了故障;redis 触发故障转移,其中一个 slave 升级为新的 master,此时新上位的master并不包含线程1写入的键值对,因此线程 2 尝试获取锁也可以成功拿到锁,此时相当于有两个线程获取到了锁,可能会导致各种预期之外的情况发生,例如最常见的脏数据。

所以Redis官方的推荐方式如下:

  • Redis 提供了 Redlock 算法,用来实现基于多个实例的分布式锁
  • 锁变量由多个实例维护,即使有实例发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作
  • Redlock算法是实现高可靠分布式锁的一种有效解决方案,可以在实际开发中使用

1.3.Redlock算法设计理解

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

Redis也提供了Redlock算法,用来实现基于多个实例的分布式锁。锁变量由多个实例维护,即使有实例发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作。Redlock算法是实现高可靠分布式锁的一种有效解决方案,可以在实际开发中使用,官网说明如下:

1.3.2.设计理念

该方案也是基于(set 加锁、Lua 脚本解锁)进行改良的,所以redis之父antirez 只描述了差异的地方,大致方案如下。假设我们有N个Redis主节点,例如 N = 5这些节点是完全独立的,我们不使用复制或任何其他隐式协调系统,

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

1
获取当前时间,以毫秒为单位;
2
依次尝试从5个实例,使用相同的 key 和随机值(例如 UUID)获取锁。当向Redis 请求获取锁时,客户端应该设置一个超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为 10 秒,则超时时间应该在 5-50 毫秒之间。这样可以防止客户端在试图与一个宕机的 Redis 节点对话时长时间处于阻塞状态。如果一个实例不可用,客户端应该尽快尝试去另外一个 Redis 实例请求获取锁;
3
客户端通过当前时间减去步骤 1 记录的时间来计算获取锁使用的时间。当且仅当从大多数(N/2+1,这里是 3 个节点)的 Redis 节点都取到锁,并且获取锁使用的时间小于锁失效时间时,锁才算获取成功;
4
如果取到了锁,其真正有效时间等于初始有效时间减去获取锁所使用的时间(步骤 3 计算的结果)。
5
如果由于某些原因未能获得锁(无法在至少 N/2 + 1 个 Redis 实例获取锁、或获取锁的时间超过了有效时间),客户端应该在所有的 Redis 实例上进行解锁(即便某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。

该方案为了解决数据不一致的问题,直接舍弃了异步复制只使用 master 节点,同时由于舍弃了 slave,为了保证可用性,引入了 N 个节点(都是master),官方建议是 5。客户端只有在满足下面的这两个条件时,才能认为是加锁成功。

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

1.3.3.解决方案

redis只支持AP,为了解决CP的风险,采用N个节点,N为奇数,官方推荐5台机器,并且5台都是master各自完全独立,不是主从或者集群。

为什么是Redis机器的台数是奇数?

  • N = 2X + 1 (N是最终部署机器数,X是容错机器数)
  • 这里的容错指的是失败了多少个机器实例后我还是可以容忍的,所谓的容忍就是数据一致性还是可以Ok的,CP数据一致性还是可以满足
  • 之所以使用奇数目的在于:最少的机器,最多的产出效果

计算案例如下:

加入在集群环境中,redis失败1台,可接受。2x+1= 1 * 1+1 =3,部署3台
加入在集群环境中,redis失败2台,可接受。2x+1 = 2 * 2+1 =5,部署5台

1.3.4.Redisson进行代码改造

上述的实现思路,在Java使用Redis的时候,则是通过Redisson 来进行 Redlock算法 的实现

Redisson是java的redis客户端之一,提供了一些api方便操作redis的接口

redisson之官网 https://redisson.org/

redisson在Github上网址 https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95

二、Redisson实现分布式锁(单机版本)

参考官方说明:https://github.com/redisson/redisson/wiki/8.-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E5%92%8C%E5%90%8C%E6%AD%A5%E5%99%A8,如下图:

2.1.引入Redisson依赖

还是利用之前的手写分布式锁的两个模块来实现(注意两个模块添加的内容一样复制即可),这时候就不需要使用我们手写的锁,使用Redisson实现锁,引入依赖如下:

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

2.2.添加Redisson单机配置文件

在config包下RedisConfig添加单机配置连接Redis的配置文件,这里的Redis是实现分布式锁的

package com.augus.redis.config;

import org.redisson.Redisson;
import org.redisson.config.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 {
    /**
     * redis序列化的工具配置类,下面这个请一定开启配置
     * 127.0.0.1:6379> keys *
     * 1) "ord102"  序列化过
     * 2) "\xac\xed\x00\x05t\x00\aord102"   没有序列化过
     * this.redisTemplate.opsForValue(); //提供了操作string类型的所有方法
     * this.redisTemplate.opsForList(); // 提供了操作list类型的所有方法
     * this.redisTemplate.opsForSet(); //提供了操作set的所有方法
     * this.redisTemplate.opsForHash(); //提供了操作hash表的所有方法
     * this.redisTemplate.opsForZSet(); //提供了操作zset的所有方法
     * @param lettuceConnectionFactory
     * @return
     */
    @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,使用GenericJackson2JsonRedisSerializer替换默认序列化
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());

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

        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }

    @Bean
    public Redisson redisson(){
        // 1. Create config object
        Config config = new Config();

        // use "rediss://" for SSL connection,设置连接的redis和操作0号库,密码为123456
        config.useSingleServer().setAddress("redis://192.168.42.136:6381").setDatabase(0).setPassword("123456");

        //返回连接对象
        return (Redisson) Redisson.create(config);
    }
}

2.3.修改service层的InventoryService,使用Redisson提交的分布式锁实现

这里不再使用之前我们手写的分布式锁来实现,而是通过Redisson实现的分布式锁来实现,代码修改如下:

package com.augus.redis.service;

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class InventoryService {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Value("${server.port}")
    private String port;

    @Resource
    private Redisson redisson;


    //最终版本实现自动续期功能的完善,后台自定义扫描程序,如果规定时间内没有完成业务逻辑,会调用加钟自动续期的脚本
    public String sale(){
        String message = "";
        //设置锁的名字
        String key = "hiRedisLock";

        //获取锁,设置锁的名字
        RLock redissonLock = redisson.getLock(key);

        //上锁
        redissonLock.lock();

        try {
            //1.查询库存
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //2.判断库存是否充足
            Integer inventoryNumber = null;

            if(result==null){
                inventoryNumber=0;
            }else {
                inventoryNumber=Integer.parseInt(result);
            }

            //3.减扣库存
            if(inventoryNumber>0){
                //将库存自减1,然后保存
                stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
                message = "卖出一件商品,库存剩余:"+inventoryNumber;
                System.out.println(message);

                // 演示自动续期的的功能
                /*try {
                    TimeUnit.SECONDS.sleep(120);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }*/
            }else {
                message = "商品已经售完";
            }

        }finally {
            //释放锁
            redissonLock.unlock();
        }
        return message+"\t"+"服务端口号:"+port;
    }
}

2.3.controller层保持不变

在InventoryController内容和之前一样如下:

package com.augus.redis.controller;

import com.augus.redis.service.InventoryService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Api(tags = "Redis分布式锁测试")
public class InventoryController {
    @Autowired
    private InventoryService inventoryService;

    @ApiOperation("扣减库存,一次卖一个")
    @GetMapping(value = "/inventory/sale")
    public String sale(){
        return inventoryService.sale();
    }
}

2.4.测试

启动两台Redis,为业务数据缓存的Redis:192.168.42.123:6379

启动redisson分布锁的redis 192.168.42.136:6381,然后通过启动两个微服务模块,然后通过jmeter脚本模拟高并发测试

查看控制台,库存被依次扣减

再次查看Redis中,库存已经变为0

三、Redisson实现分布式锁(多机版本)

3.1.通过docker创建多台Redis的

上一个章节中Redisson的分布式锁,是单个Redis实现了,而下面案例则根据官网建议使用五台Redis来实现分布式锁的多机版本,保证容错,下面通过docker创建五台Redis实例,代码如下:

docker run -p 6381:6379 --name redis-master-1 -d redis --requirepass 123456
docker run -p 6382:6379 --name redis-master-2 -d redis --requirepass 123456
docker run -p 6383:6379 --name redis-master-3 -d redis --requirepass 123456
docker run -p 6384:6379 --name redis-master-4 -d redis --requirepass 123456
docker run -p 6385:6379 --name redis-master-5 -d redis --requirepass 123456

进入Redis容器方式

docker exec -it redis-master-1 /bin/bash   或者 docker exec -it redis-master-1 redis-cli -a 123456
docker exec -it redis-master-2 /bin/bash   或者 docker exec -it redis-master-2 redis-cli -a 123456
docker exec -it redis-master-3 /bin/bash   或者 docker exec -it redis-master-3 redis-cli -a 123456
docker exec -it redis-master-4 /bin/bash   或者 docker exec -it redis-master-4 redis-cli -a 123456
docker exec -it redis-master-5 /bin/bash   或者 docker exec -it redis-master-5 redis-cli -a 123456

3.2.在两个模块中引入依赖

在pom.xml中引入依赖:

<!--引入redisson-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.20.1</version>
        </dependency>

       <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.12.0</version>
        </dependency>

3.3.在配置文件中添加业务所需Redis

在application.properties中内容如下(和之前一样的):

server.port=7777
spring.application.name=redis_distributed_lock

# ========================swagger2=====================
# http://localhost:7777/swagger-ui.html
spring.swagger2.enabled=true
spring.mvc.pathmatch.matching-strategy=ant_path_matcher

# ========================redis单机=====================
spring.redis.database=0
spring.redis.host=192.168.42.132
spring.redis.port=6379
spring.redis.password=123456
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0

3.4.修改RedisConfig中配置

这里创建5个方法对应连接5台Redis主机,实现RedLock

package com.augus.redis.config;

import org.redisson.Redisson;
import org.redisson.config.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 {
    /**
     * redis序列化的工具配置类,下面这个请一定开启配置
     * 127.0.0.1:6379> keys *
     * 1) "ord102"  序列化过
     * 2) "\xac\xed\x00\x05t\x00\aord102"   没有序列化过
     * this.redisTemplate.opsForValue(); //提供了操作string类型的所有方法
     * this.redisTemplate.opsForList(); // 提供了操作list类型的所有方法
     * this.redisTemplate.opsForSet(); //提供了操作set的所有方法
     * this.redisTemplate.opsForHash(); //提供了操作hash表的所有方法
     * this.redisTemplate.opsForZSet(); //提供了操作zset的所有方法
     * @param lettuceConnectionFactory
     * @return
     */
    @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,使用GenericJackson2JsonRedisSerializer替换默认序列化
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());

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

        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }

    @Bean
    public Redisson redissonClient1(){
        // 1. Create config object
        Config config = new Config();

        // use "rediss://" for SSL connection
        config.useSingleServer().setAddress("redis://192.168.42.136:6381").setDatabase(0).setPassword("123456");

        //返回连接对象
        return (Redisson) Redisson.create(config);
    }

    @Bean
    public Redisson redissonClient2(){
        // 1. Create config object
        Config config = new Config();

        // use "rediss://" for SSL connection
        config.useSingleServer().setAddress("redis://192.168.42.136:6382").setDatabase(0).setPassword("123456");

        //返回连接对象
        return (Redisson) Redisson.create(config);
    }

    @Bean
    public Redisson redissonClient3(){
        // 1. Create config object
        Config config = new Config();

        // use "rediss://" for SSL connection
        config.useSingleServer().setAddress("redis://192.168.42.136:6383").setDatabase(0).setPassword("123456");

        //返回连接对象
        return (Redisson) Redisson.create(config);
    }

    @Bean
    public Redisson redissonClient4(){
        // 1. Create config object
        Config config = new Config();

        // use "rediss://" for SSL connection
        config.useSingleServer().setAddress("redis://192.168.42.136:6384").setDatabase(0).setPassword("123456");

        //返回连接对象
        return (Redisson) Redisson.create(config);
    }
    @Bean
    public Redisson redissonClient5() {
        // 1. Create config object
        Config config = new Config();

        // use "rediss://" for SSL connection
        config.useSingleServer().setAddress("redis://192.168.42.136:6385").setDatabase(0).setPassword("123456");

        //返回连接对象
        return (Redisson) Redisson.create(config);
    }
}

3.5.修改InventoryService中配置

这里使用redisson提供的联锁机制实现,官方说明如下:

 代码参数:

package com.augus.redis.service;

import org.redisson.RedissonMultiLock;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

@Service
public class InventoryService {
    //定义锁的名字
    public static final String CACHE_KEY_REDLOCK = "hiRedLock";

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Value("${server.port}")
    private String port;

    @Resource
    RedissonClient redissonClient1;

    @Resource
    RedissonClient redissonClient2;

    @Resource
    RedissonClient redissonClient3;

    @Resource
    RedissonClient redissonClient4;

    @Resource
    RedissonClient redissonClient5;


    //最终版本实现自动续期功能的完善,后台自定义扫描程序,如果规定时间内没有完成业务逻辑,会调用加钟自动续期的脚本
    public String sale(){
        String message = "";

        RLock lock1 = redissonClient1.getLock(CACHE_KEY_REDLOCK);
        RLock lock2 = redissonClient2.getLock(CACHE_KEY_REDLOCK);
        RLock lock3 = redissonClient3.getLock(CACHE_KEY_REDLOCK);
        RLock lock4 = redissonClient4.getLock(CACHE_KEY_REDLOCK);
        RLock lock5 = redissonClient5.getLock(CACHE_KEY_REDLOCK);

        //创建联锁
        RedissonMultiLock redissonMultiLock = new RedissonMultiLock(lock1, lock2, lock3, lock4, lock5);

        // 同时加锁:lock1 lock2 lock3 lock4 lock5
        // 红锁在大部分节点上加锁成功就算成功。
        redissonMultiLock.lock();

        try {
            //1.查询库存
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //2.判断库存是否充足
            Integer inventoryNumber = null;

            if(result==null){
                inventoryNumber=0;
            }else {
                inventoryNumber=Integer.parseInt(result);
            }

            //3.减扣库存
            if(inventoryNumber>0){
                //将库存自减1,然后保存
                stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
                message = "卖出一件商品,库存剩余:"+inventoryNumber;
                System.out.println(message);

                // 演示自动续期的的功能
                try {
                    TimeUnit.SECONDS.sleep(120);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }else {
                message = "商品已经售完";
            }

        }finally {
            //释放锁
            redissonMultiLock.unlock();
        }
        return message+"\t"+"服务端口号:"+port;
    }
}

3.6.测试

这里需要说明之前使用的两个模块通过nginx实现的负载均衡,这里还是这两个模块上述的都安装上面的内容修改即可,通过jmeter执行脚本模块高并发下访问,我在代码中添加了休眠等待就是为了看到锁的创建

 同时查看该锁过期时间,可以自动续期

查看控制台,液位出现超卖的现象

同时查看Redis中,库存已经减扣完毕

posted @ 2023-04-14 12:40  酒剑仙*  阅读(418)  评论(0)    收藏  举报