SpringBoot中使用Redis实现分布式锁

前言

在单机环境中,应用是同一进程下,我们只需要保证单进程、多线程线程安全性即可,通过Java提供的volatile、ReentrantLock、synchronized 以及 concurrent 并发包下一些线程安全的类等就可以做到。但是在多机部署的环境中,不同机器不同进程,就需要在多进程下保证线程安全性,因此,分布式锁应允而生。本章节主要介绍SpringBoot中如何实现Redis实现分布式锁的?

1.使用StringRedisTemplate实现(未实现库存扣减功能)

2.使用Redisson实现(实现了库存扣减功能)

1.使用StringRedisTemplate实现

我们先模拟下并发场景:假设redis库存stock初始值为100,现在有5个并发的线程同时对库存进行扣减。

1、引入依赖

<!-- Reids注解 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2、application.yml 配置文件

server:
  port: 9999
spring:
  data:
    redis:
      database: 15 #redis库
      host: 127.0.0.1 #服务器地址
      //password: "0211" #密码
      port: 6379 #端口号
      jedis:
        pool:
          max-active: 8 #连接池最大连接数
          max-idle: 8 #连接池最大空闲连接数
          min-idle: 0 #连接池最小空闲连接数
          max-wait: -1 #端口号
      timeout: 1000000 #连接超时时间(毫秒)

3、示例代码

@RestController
public class RedisLockController {
    @Autowired
    private StringRedisTemplate redisTemplate;
    /**
     * 模拟下单减库存的场景
     * @return
     */
    @RequestMapping(value = "/deduct_stock")
    public String deductStock() {
        redisTemplate.opsForValue().set("stock", String.valueOf(100));
        for (int i = 0; i < 5; i++) {
            CompletableFuture.runAsync(() -> {
                // 从redis 中拿当前库存的值
                int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
                if (stock > 0) {
                    int realStock = stock - 1;
                    redisTemplate.opsForValue().set("stock", realStock + "");
                    System.out.println("扣减成功,剩余库存:" + realStock);
                } else {
                    System.out.println("扣减失败,库存不足");
                }
            });
        }
        return "success";
    }
}

启动redis、设置密码授权、启动后端服务,使用postman请求:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

正常执行完,我们看到了redis中的值应该为95,但实际上不是,结果却是99。

这个时候,大家都看出来,出现了线程安全的问题,我们首先能想到的肯定是给它加synchronized锁。

是的,没问题,但是我们知道,synchronized锁是属于JVM级别的,也就是我们所谓的“单机锁”,如果是多机部署的环境中,还能保证数据的一致性吗?

答案肯定是不能的。

这个时候,就需要用到了我们Redis分布式锁。

Redis分布式锁

@RestController
public class RedisLockController {

    @Autowired
    private StringRedisTemplate redisTemplate;
    /**
     * 模拟下单减库存的场景
     *
     * @return
     */
    @RequestMapping(value = "/deduct_stock")
    public String deductStock() {
        redisTemplate.opsForValue().set("stock-c", String.valueOf(100));
        for (int i = 0; i < 5; i++) {
            CompletableFuture.runAsync(() -> {
                String key = "lock_key";
                String value = "ID_PREFIX" + Thread.currentThread().getId();
                //key 要锁住的key;value 值;Duration.ofSeconds(60) 过期时间,单位:秒
                Boolean success = redisTemplate.opsForValue().setIfAbsent(key, value, Duration.ofSeconds(60));
                if (success){
                    // 从redis 中拿当前库存的值
                    int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock-c"));
                    if (stock > 0) {
                        int realStock = stock - 1;
                        redisTemplate.opsForValue().set("stock-c", realStock + "");
                        System.out.println("扣减成功,剩余库存:" + realStock);
                    } else {
                        System.out.println("扣减失败,库存不足");
                    }
                    //释放锁
                    redisTemplate.delete(key);
                }
            });
        }
        return "success";
    }
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Boolean success = redisTemplate.opsForValue().setIfAbsent(key, value, Duration.ofSeconds(60));

如果success返回值为true,代表之前redis中没有key为local_key的值,表示加锁成功;反之为false 则代表已经存在当前key,则不执行扣减逻辑。

以上代码用我们的正常思维考虑,应该是没问题,但是并不完美,总结为以下几点:1、如果执行到业务代码出现异常,那释放锁的代码就无法执行,怎么解决?(可以加try … finally解决)

2、如果请求1 首先加锁需要执行15秒,过期时间设置的是10秒;请求2进入加锁执行到5秒的时候,请求1 执行完成要进行锁的释放,此时释放的就不是自己的锁,怎么解决?(释放锁的时,增加value值的判断)

问题1、2 的优化代码如下:

@RestController
public class RedisLockController {

    @Autowired
    private StringRedisTemplate redisTemplate;
/**
     * 模拟下单减库存的场景
     *
     * @return
     */
    @RequestMapping(value = "/deduct_stock")
    public String deductStock() {
        redisTemplate.opsForValue().set("stock-d", String.valueOf(100));
        for (int i = 0; i < 5; i++) {
            CompletableFuture.runAsync(() -> {
                String key = "lock_key";
                String value = "ID_PREFIX" + Thread.currentThread().getId();
                //key 要锁住的key;value 值;
                //Duration.ofSeconds(60) 过期时间,单位:秒
                Boolean success = redisTemplate.opsForValue().setIfAbsent(key, value, Duration.ofSeconds(60));
                if (success) {
                    try {
                        // 从redis 中拿当前库存的值
                        int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock-d"));
                        if (stock > 0) {
                            int realStock = stock - 1;
                            redisTemplate.opsForValue().set("stock-d", realStock + "");
                            System.out.println("扣减成功,剩余库存:" + realStock);
                        } else {
                            System.out.println("扣减失败,库存不足");
                        }

                    } finally {//finally解决无法正常释放锁问题
                      //currentValue.equals(value) 解决请求释放锁的时候,
                      //释放掉的不是自己锁的问题
                        String currentValue = redisTemplate.opsForValue().get(key);
                        if (currentValue != null && currentValue.equals(value)) {
                            //释放锁
                            redisTemplate.delete(key);
                        }

                    }
                }
            });
        }
        return "success";
    }
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.使用Redisson实现

1、引入依赖

<!-- Reids注解 StringRedisTemplate-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- redisson依赖 -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.20.0</version>
</dependency>

2、添加application.yml配置信息

server:
  port: 9999
spring:
  data:
    redis:
      database: 0 #redis库
      host: 127.0.0.1 #服务器地址
      #password: "0211" #密码
      port: 6379 #端口号
      jedis:
        pool:
          max-active: 8 #连接池最大连接数
          max-idle: 8 #连接池最大空闲连接数
          min-idle: 0 #连接池最小空闲连接数
          max-wait: -1 #端口号
      timeout: 100000000 #连接超时时间(毫秒)

3、添加Redis配置信息

@Configuration
public class RedisConfig {
    @Bean
    public RedissonClient redisson(){
        // useSingleServer 单机模式,还存在其他模式
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(1);
        return Redisson.create(config);
    }
}

4、添加库存扣减接口

@RestController
public class RedisLockController {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private RedissonClient redisson;

    @RequestMapping(value = "/deduct_stock")
    public String deductStock() {
        redisTemplate.opsForValue().set("stock", String.valueOf(100));
        for (int i = 0; i < 5; i++) {
            int finalI = i;
            CompletableFuture.runAsync(() -> {
                String key = "lock_key";
                //1、获取锁对象
                RLock redissonLock = redisson.getLock(key);
                try {
                //2、加锁
                redissonLock.lock();
                // 从redis 中拿当前库存的值
                int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
                if (stock > 0) {
                int realStock = stock - 1;
                redisTemplate.opsForValue().set("stock", realStock + "");    
                        System.out.println("线程" + finalI + "扣减成功,剩余库存:" + realStock);
                    } else {
                        System.out.println("扣减失败,库存不足");
                    }
                } finally {
                    //3、释放锁
                    redissonLock.unlock();
                }
            });
        }
        return "success";
    }
}

5、启动后端服务,启动Redis服务,客户端设置密码、授权连接Reids服务

在这里插入图片描述

设置密码:config set requirepass 0211

授权: config get requirepass

在这里插入图片描述
在这里插入图片描述

6、调用库存扣减接口测试

接口地址:http://localhost:9999/deduct_stock

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Redisson增加了锁续时的机制,正好用于解决我们使用StringRedisTemplate遗留下来的无法续时的问题。由于Redisson太过复杂,所以我们今天只针对如何加锁,如何进行锁续时原理进行分析。

以下是Redisson的大致实现原理:

在这里插入图片描述

假设现在一个线程过来,尝试过去锁:

2.1、获取锁失败,证明有别的线程正在持有锁,于是一直自旋尝试获取。

2.2、如果获取锁成功,则执行加锁逻辑,另外开启一个线程(看门狗)每隔10s中判断锁是否还被当前线程持有,如果是就延长锁的过期时间,这就是Redisson的锁续时机制。

2.3、程序运行结束后,释放锁。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值