分布式锁 redis redisson

一  高并发情况 模拟单机并发环境

使用测试工具jmeter实现功能测试

步骤一  创建线程组

步骤二  更改线程数量

步骤三  添加请求方式,并设置参数

步骤四  添加请求结果页面

步骤五  开启后端服务点击测试

我们后端测试内容,每次执行程序就将redis里面的string类型的key:num加一。发现问题。在多线程的时候会重复加。

二  单机并发解决方法---本地锁

就是在改变num值的方法加上  synchronized  同步代码块。结果当然没问题。但是上面测试方式,在一个服务器里面进行的测试,单机版服务,本地锁才能发挥作用。到了微服务,分布式,集群的情况,本地锁就失去了作用

三  高并发环境  模拟集群并发环境

使用idea部署集群效果,里面还是本地锁

步骤一  修改nacos配置中心端口配置,端口部分注释掉

我们使用service-order这个微服务来做测试

步骤二   项目配置文件中添加端口号 bootstrap.properties

将我们的service-order服务在 bootstrap.properties  下面设置端口号,提高他的优先级

步骤三 操作idea添加多个端口号

将service-order复制多份,并改他的端口号。这样就有集群效果。复制在我鼠标停留的地方

步骤四  网关模块配置文件添加路由规则

步骤五  启动集群服务,还需要启动网关服务

步骤六  开始测试

1  将请求的端口号设置成网关的,并设置线程数为1000

2  修改num的值为0,用于测试

3  启动请求,查看num值.我们期望拿到的是1000,但是只有519.很明显本地锁在这里失效了。应该说,本地锁对当前的服务生效了,但是对多服务失效

四  redis分布式锁

1  加锁流程(有问题)

在我线程1 ,我用setnx方法加锁,将num的值设置成1。此时,我在线程2中用setnx方法将num设置成2.此时线程2的操作不会成功。线程一设置完后,我用del方法将锁给删除了之后,我在线程2再次设置num的值,才会成功。

2  代码实现(有问题)

@Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    public void testLock() {
        //从redis里面获取数据
        //1 获取当前锁  setnx
        Boolean ifAbsent = redisTemplate.opsForValue().setIfAbsent("lock", "lock");

        //2 如果获取到锁,从redis获取数据 数据+1 放回redis里面
        if(ifAbsent) {
            //获取锁成功,执行业务代码
            //1.先从redis中通过key num获取值  key提前手动设置 num 初始值:0
            String value = redisTemplate.opsForValue().get("num");
            //2.如果值为空则非法直接返回即可
            if (StringUtils.isBlank(value)) {
                return;
            }
            //3.对num值进行自增加一
            int num = Integer.parseInt(value);
            redisTemplate.opsForValue().set("num", String.valueOf(++num));

            //3 释放锁
            redisTemplate.delete("lock");
        } else {
            try {
                Thread.sleep(100);
                this.testLock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
 

3  问题解决

我们在上面加锁的时候如果在加锁和删除锁之间出现了bug,那么我们的锁将会一直在。

解决:

我们可以用try-catch-finally来释放锁

我们也可以设置过期时间,到时间立马删除锁。

4  设置过期时间(也有问题)

在setnx设置锁的地方设置过期时间,其余代码一样。这里设置了三秒

@Override
    public void testLock() {
        //从redis里面获取数据
        //1 获取当前锁  setnx  + 设置过期时间
//        Boolean ifAbsent = redisTemplate.opsForValue().setIfAbsent("lock", "lock");
        Boolean ifAbsent =
                redisTemplate.opsForValue()
                        .setIfAbsent("lock", "lock",3, TimeUnit.SECONDS);
        //2 如果获取到锁,从redis获取数据 数据+1 放回redis里面
        if(ifAbsent) {
            //获取锁成功,执行业务代码
            //1.先从redis中通过key num获取值  key提前手动设置 num 初始值:0
            String value = redisTemplate.opsForValue().get("num");
            //2.如果值为空则非法直接返回即可
            if (StringUtils.isBlank(value)) {
                return;
            }
            //3.对num值进行自增加一
            int num = Integer.parseInt(value);
            redisTemplate.opsForValue().set("num", String.valueOf(++num));
//            ???????bug???????
            //3 释放锁
            redisTemplate.delete("lock");
        } else {
            try {
                Thread.sleep(100);
                this.testLock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

5  还有一件事!!!

使用setnx+过期时间实现分布式锁,存在问题:删除不是自己的锁,锁误删

场景:如果业务逻辑的执行时间是7s。执行流程如下

1. index1业务逻辑没执行完,3秒后锁被自动释放。

2. index2获取到锁,执行业务逻辑,3秒后锁被自动释放。

3. index3获取到锁,执行业务逻辑

 index1业务逻辑执行完成,开始调用del释放锁,这时释放的是index3的锁, 导致index3的业务只执行1s就被别人释放。

最终等于没锁的情况

6  改正(有问题)

给锁加一个自己独一无二的名字,删锁的时候先判断删的是不是自己的锁.设置value为uuid

 @Autowired
    private StringRedisTemplate redisTemplate;

    //uuid防止误删
    @Override
    public void testLock() {
        //从redis里面获取数据
        String uuid = UUID.randomUUID().toString();
        //1 获取当前锁  setnx  + 设置过期时间
        //        Boolean ifAbsent = redisTemplate.opsForValue().setIfAbsent("lock", "lock");
        Boolean ifAbsent =
                redisTemplate.opsForValue()
                        .setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS);

        //2 如果获取到锁,从redis获取数据 数据+1 放回redis里面
        if (ifAbsent) {
            //获取锁成功,执行业务代码
            //1.先从redis中通过key num获取值  key提前手动设置 num 初始值:0
            String value = redisTemplate.opsForValue().get("num");
            //2.如果值为空则非法直接返回即可
            if (StringUtils.isBlank(value)) {
                return;
            }
            //3.对num值进行自增加一
            int num = Integer.parseInt(value);
            redisTemplate.opsForValue().set("num", String.valueOf(++num));
            //出现异常

            //3 释放锁
            String redisUuid = redisTemplate.opsForValue().get("lock");
            if (uuid.equals(redisUuid)) {
                redisTemplate.delete("lock");
            }

        } else {
            try {
                Thread.sleep(100);
                this.testLock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

7  最后一遍找错!

通过uuid防止误删,但是还是存在问题,他不具备原子性的。

如果在第一行代码执行后放行,突然间锁过时了或者发生其他事,导致锁被另外一个线程b得到了,那么第二行代码就删了那个线程b的锁。

8  最终版(正确版本)(看就看这个版本)(上面都是推导过程)

我们将删除锁的代码用lua脚本代替。我们需要在执行删除锁的时候保证只有一个线程在执行任务,这就需要程序的原子性

Lua 中的原子性

在 Lua 脚本中,原子性并不是直接由语言本身提供的特性,而是由 Lua 的执行环境和与其交互的系统决定的。例如:

  1. Lua 虚拟机:Lua 的虚拟机以解释器的形式逐行执行脚本代码。Lua 本身不会将脚本的执行分割成更小的操作,因此在执行 Lua 代码时,每个语句通常被视为原子操作。

  2. 数据一致性:Lua 提供了事务性和一致性操作的工具,例如在表操作中,如果你在一个表中进行插入或更新操作,这些操作要么完全成功,要么在发生错误时不会有部分数据修改。

  3. 并发和同步:Lua 本身并不原生支持多线程。Lua 的标准库中没有提供多线程支持,通常在嵌入 Lua 的应用程序中,线程管理和原子性保障由宿主程序负责。比如在多线程环境中,Lua 代码通常需要与宿主程序进行适当的同步,以保证线程安全。

  4. 元表和元方法:Lua 的元表和元方法允许你在操作表时实现自定义的行为。虽然 Lua 的表操作在大多数情况下是原子的,但如果你使用元表和元方法来定义复杂的行为,可能会涉及到一些操作的原子性问题,这取决于你如何设计和实现这些元方法。

事务性和错误处理

在 Lua 中处理事务性操作和错误处理通常依赖于使用 pcall(保护调用)函数来捕捉运行时错误pcall 允许你安全地执行代码块,并且如果出现错误,不会导致整个程序崩溃。这种机制可以帮助你在一定程度上实现操作的“原子性”,通过确保即使在执行过程中发生错误,也可以控制错误的处理方式。

9  最终版本执行代码

我们用lua脚本代替原来的java语言来删除锁

//lua脚本保证原子性
@Override
public void testLock() {
    //从redis里面获取数据
    String uuid = UUID.randomUUID().toString();
    //1 获取当前锁  setnx  + 设置过期时间
    //        Boolean ifAbsent = redisTemplate.opsForValue().setIfAbsent("lock", "lock");
    Boolean ifAbsent =
            redisTemplate.opsForValue()
                    .setIfAbsent("lock", uuid,3, TimeUnit.SECONDS);

    //2 如果获取到锁,从redis获取数据 数据+1 放回redis里面
    if(ifAbsent) {
        //获取锁成功,执行业务代码
        //1.先从redis中通过key num获取值  key提前手动设置 num 初始值:0
        String value = redisTemplate.opsForValue().get("num");
        //2.如果值为空则非法直接返回即可
        if (StringUtils.isBlank(value)) {
            return;
        }
        //3.对num值进行自增加一
        int num = Integer.parseInt(value);
        redisTemplate.opsForValue().set("num", String.valueOf(++num));
        //出现异常

        //3 释放锁 lua脚本实现
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        //lua脚本
        String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                "then\n" +
                "    return redis.call(\"del\",KEYS[1])\n" +
                "else\n" +
                "    return 0\n" +
                "end";
        redisScript.setScriptText(script);
        //设置返回结果
        redisScript.setResultType(Long.class);
        redisTemplate.execute(redisScript, Arrays.asList("lock"),uuid);
        
    } else {
        try {
            Thread.sleep(100);
            this.testLock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

10  总结

10.1  加锁并设置过期时间

// 1. 从Redis中获取锁,set k1 v1 px 20000 nx

String uuid = UUID.randomUUID().toString();

Boolean lock = this.redisTemplate.opsForValue()

      .setIfAbsent("lock", uuid, 2, TimeUnit.SECONDS);

10.2  使用lua释放锁

// 2. 释放锁 del

String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

// 设置lua脚本返回的数据类型

DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();

// 设置lua脚本返回类型为Long

redisScript.setResultType(Long.class);

redisScript.setScriptText(script);

redisTemplate.execute(redisScript, Arrays.asList("lock"),uuid);

```

10.3  重试

Thread.sleep(500);

testLock();

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

第一个:互斥性,在任何时刻,只有一个客户端能持有锁。

第二个:不会发生死锁,即使有一个客户端在获取锁操作时候崩溃了,也能保证其他客户端能获取到锁。

第三个:解铃还须系铃人,解锁加锁必须同一个客户端操作

第四个:加锁和解锁必须具备原子性

五  Redisoon分布式锁(框架)

1 概述

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。

Redisson的宗旨是:促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。

2  快速入门

2.1  引入依赖,版本号自己写

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

2.2  创建redisson配置类

注意读取配置的时候留意名字

package com.atguigu.daijia.common.config.redisson;


import lombok.Data;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.config.SingleServerConfig;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;

@Data
@Configuration
@ConfigurationProperties(prefix = "spring.data.redis")
public class RedissonConfig {

    private String host;

    private String password;

    private String port;

    private int timeout = 3000;
    private static String ADDRESS_PREFIX = "redis://";

    /**
     * 自动装配
     *
     */
    @Bean
    RedissonClient redissonSingle() {
        Config config = new Config();

        if(!StringUtils.hasText(host)){
            throw new RuntimeException("host is  empty");
        }
        SingleServerConfig serverConfig = config.useSingleServer()
                .setAddress(ADDRESS_PREFIX + this.host + ":" + port)
                .setTimeout(this.timeout);
        if(StringUtils.hasText(this.password)) {
            serverConfig.setPassword(this.password);
        }
        return Redisson.create(config);
    }
}

2.3  使用redisson加锁释放锁

有三种加锁方式自己选择

@Autowired
    private RedissonClient redissonClient;

    //uuid防止误删
    @Override
    public void testLock() {
        //1 通过redisson创建锁对象
        RLock lock = redissonClient.getLock("lock1");

        //2 尝试获取锁
        //(1) 阻塞一直等待直到获取到,获取锁之后,默认过期时间30s
        lock.lock();

        //(2) 获取到锁,锁过期时间10s
        // lock.lock(10,TimeUnit.SECONDS);

        //(3) 第一个参数获取锁等待时间
        //    第二个参数获取到锁,锁过期时间
        //        try {
        //            // true
        //            boolean tryLock = lock.tryLock(30, 10, TimeUnit.SECONDS);
        //        } catch (InterruptedException e) {
        //            throw new RuntimeException(e);
        //        }
        
        //获取锁成功,执行业务代码
        //1.先从redis中通过key num获取值  key提前手动设置 num 初始值:0
        String value = redisTemplate.opsForValue().get("num");
        //2.如果值为空则非法直接返回即可

        //3.对num值进行自增加一
        int num = Integer.parseInt(value);
        redisTemplate.opsForValue().set("num", String.valueOf(++num));

        //4 释放锁
        lock.unlock();
    }

  • 27
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值