SpringBoot整合Redis实现分布式锁

文章详细介绍了如何在SpringBoot应用中利用Redis实现分布式锁,包括基于setnx命令的锁、设置锁过期时间、锁续期的方法,以及演示了使用Redisson库处理锁的原子性问题,确保高可用和高性能。还讨论了分布式锁应具备的条件和可能遇到的问题,如死锁和超买问题,并提供了相应的解决方案。
摘要由CSDN通过智能技术生成

SpringBoot整合Redis实现分布式锁

在单体项目中,我们通过锁解决的是控制共享资源访问 的问题,而分布式锁,就是解决分布式系统中控制共享资源访问 的问题,与单体应用不同的是,分布式系统中竞争共享资源的最小粒度从线程升级成了进程 。一个分布式锁应该具备下列条件:

1. 在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行
2. 高可用的获取与释放锁
3. 高性能的获取与释放锁
4. 具备可重入特性(可理解为重新进入,由多于一根任务并发使用,而不必担心数据错误)
5. 具备锁失效机制,即自动解锁,防止死锁
6. 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败

分布式锁实现方式:

1. 基于数据库实现分布式锁
2. 基于zookeeper实现分布式锁
3. 基于redis实现分布式锁  

基于redis 的分布式锁

常用命令:

1. 基于setnx命令的分布式锁
setnx: set if not exist ,当且仅当key不存在时,将key的值设置为val,key存在时,不做任何动作
命令格式: setnx lock value
(1)存在的问题:假设线程获取了锁之后,在执行任务的过程中挂掉,来不及显示地执行del命令释放锁,那么竞争该锁的线程都会执行不了,产生死锁的情况。
(2)解决方案:设置锁超时时间
2. 基于setnx命令加锁并设置锁过期时间
命令格式: set key value nx ex expireTime
(1)存在问题:

① 假如线程A成功得到了锁,并且设置的超时时间是 30 秒。如果某些原因导致线程 A 执行的很慢,过了 30 秒都没执行完,这时候锁过期自动释放,线程 B 得到了锁。

② 随后,线程A执行完任务,接着执行del指令来释放锁。但这时候线程 B 还没执行完,线程A实际上删除的是线程B加的锁。

(2)解决方案:

可以在 del 释放锁之前做一个判断,验证当前的锁是不是自己加的锁。在加锁的时候把当前的线程 ID 当做value,并在删除之前验证 key 对应的 value 是不是自己线程的 ID。但是,这样做其实隐含了一个新的问题,get操作、判断和释放锁是两个独立操作,不是原子性。对于非原子性的问题,我们可以使用Lua脚本来确保操作的原子性
3. 锁续期

让获得锁的线程开启一个守护线程,用来给快要过期的锁“续期

① 假设线程A执行了29 秒后还没执行完,这时候守护线程会执行 expire 指令,为这把锁续期 20 秒。守护线程从第 29 秒开始执行,每 20 秒执行一次。

② 情况一:当线程A执行完任务,会显式关掉守护线程。

③ 情况二:如果服务器忽然断电,由于线程 A 和守护线程在同一个进程,守护线程也会停下。这把锁到了超时的时候,没人给它续命,也就自动释放了。
4. 演示代码

演示代码如下:
pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <artifactId>spring-boot-starter-parent</artifactId>
        <groupId>org.springframework.boot</groupId>
        <version>2.7.0</version>
    </parent>
    <groupId>com.young</groupId>
    <artifactId>redisson01</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.18.1</version>
        </dependency>
    </dependencies>
    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

</project>

application.yml

spring:
  redis:
    host: localhost
    port: 3306
    jedis:
      pool:
        min-idle: 0
        max-idle: 8
        max-wait: 1000
        max-active: 8
    timeout: 5000
server:
  port: 8090

Application.java

package com.young;

import org.redisson.Redisson;
import org.redisson.config.Config;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class,args);
    }
    @Bean
    public Redisson redisson(){
        //此时为单机模式
        Config config=new Config();
        config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);
        return (Redisson) Redisson.create(config);
    }
}

IndexController.java

package com.young;

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

@RestController
public class IndexController {
    @Autowired
    private Redisson redisson;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    //高并发下会出现超买,没有加锁
    @GetMapping("/reduct01")
    public String reduct01(){
        Integer stock=Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
        if (stock<0){
            System.out.println("扣减失败,库存不足");
        }else{
            Integer realStock=stock-1;
            stringRedisTemplate.opsForValue().set("stock",realStock+"");
            System.out.println("扣减成功,剩余库存:"+realStock);
        }
        return "end";
    }

    //高并发下单机模式不会出现超买,但是在分布式环境下,仍然会出现超买的现象
    @GetMapping("/reduct02")
    public String reduct02(){
        synchronized (this) {
            Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            if (stock < 0) {
                System.out.println("扣减失败,库存不足");
            } else {
                Integer realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + "");
                System.out.println("扣减成功,剩余库存:" + realStock);
            }
        }
        return "end";
    }

    //通过setnx key value命令,对商品库存进行加锁
    @GetMapping("/reduct03")
    public String reduct03(){

        //这里的key一般设置为和商品有关的key,以提高性能
        String lockKey="product_01";

        //这里可以将value设置为线程id
        String value=UUID.randomUUID().toString();

//        stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"setnx"); -> setnx key value
//        stringRedisTemplate.expire(lockKey,10,TimeUnit.SECONDS);

        //设置setnx和超时时间要原子操作
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, value, 10, TimeUnit.SECONDS);

        if (!flag){
            //加锁失败
            return "error code";
        }

        /**
         * 因为在操作的过程中,可能会抛出异常,因此我们要用try将代码块包起来,然后在finally中,释放锁,否则会出现因为异常退出,但锁在redis中存在,从而
         * 使其他线程在访问该商品时加锁失败,无法顺利扣减库存
         */
        try {
            Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            if (stock < 0) {
                System.out.println("扣减失败,库存不足");
            } else {
                Integer realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + "");
                System.out.println("扣减成功,剩余库存:" + realStock);
            }
        }finally {
            //双重保证,将value设置为当前线程的id,保证锁不会被其他线程删掉
            if (value.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
                //删除锁
                stringRedisTemplate.delete(lockKey);
            }
        }
        return "end";
    }

    /**
     * 在上面的reduct03代码中,我们通过setnx key value进行加锁,我们设置的超时时间是10秒,假设当前商品库存为100,现在线程A访问该方法,获取到
     * 锁,然后在执行业务代码时,执行的时间超过了10秒,此时锁过期了,这时线程B访问该方法,获取到锁,获取商品库存为100,这样当A和B线程都执行完毕后,
     * 商品的库存实际上,只减少了1,也就是变成99,从而导致超买,那么我们需要在锁过期前,进行续期,因此使用redisson
     * @return
     */
    @GetMapping("/reduct04")
    public String reduct04(){
        String lockKey="product_01";

        RLock redissonLock = redisson.getLock(lockKey);

        try {
            redissonLock.lock();
            Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            if (stock < 0) {
                System.out.println("扣减失败,库存不足");
            } else {
                Integer realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + "");
                System.out.println("扣减成功,剩余库存:" + realStock);
            }
        }finally {
            redissonLock.unlock();
            //删除锁
            stringRedisTemplate.delete(lockKey);
        }
        return "end";
    }

    /**
     * 考虑一种情况,我们使用redis集群,当设置key的时候,主节点甲会异步地将数据同步到其他从节点,假设现在线程A扣减商品product1库存,那么它在主节点A中设置可以,
     * 但是假设主节点A在同步数据之前挂了,数据没同步到从节点,现在从节点乙推选为主节点,线程B访问主节点乙,然后也是访问商品product1,设置key值,然后假设A、B线
     * 程同时结束,这个时候,会出现超买问题。根本原因是Redis主从同步是异步的,我们可以使用zookeeper来加分布式锁,zookeeper在主从同步时,主节点设置成功后,会
     * 先同步给从节点,只有集群中有一半以上同步后,才返回true,而即使主节点挂了,在推选主节点的时候,选择的也会是数据最完善的那个,因此不会出现刚才的问题,但带来
     * 的问题就是,zookeeper的性能比redis差
     */

//    @RequestMapping("/deduct_stock")
//    public String deductStock(){
        //锁住的lock要以资源为名,提高效率
//        String lockKey="product_001";
//
//        //可以设置为线程id
//        String clientId= UUID.randomUUID().toString();
//
//        //jedis.setnx(key,value)
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "zhuge");
        stringRedisTemplate.expire(lockKey,10, TimeUnit.SECONDS);
//
//        //加锁和设置过时时间要原子操作,考虑问题,执行时间大于超时时间?
//        Boolean result=stringRedisTemplate.opsForValue().setIfAbsent(lockKey,clientId,10,TimeUnit.SECONDS);
//
//        if (!result){
//            return "error_code";
//        }
//
//        try{
//            int stock=Integer.parseInt(stringRedisTemplate.opsForValue().get("stock");
//            if (stock>0){
//                int realStock=stock-1;
//                stringRedisTemplate.opsForValue().set("stock",realStock+"");
//                System.out.println("扣减成功,剩余库存:"+realStock);
//            }else{
//                System.out.println("扣减失败,库存不足");
//            }
//        }finally {
//            if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
//                //当前线程的id和取出来的id相同时,才解锁
//                stringRedisTemplate.delete(lockKey);
//            }
//        }
//        return "end";
//
//        String lockKey="product_001";
//
//        RLock redissonLock = redisson.getLock(lockKey);
//
//        try{
//            redissonLock.lock();
//            int stock=Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
//            if (stock>10){
//                int realStock=stock-1;
//                stringRedisTemplate.opsForValue().set("stock",realStock+"");
//                System.out.println("扣减成功,剩余库存:"+realStock);
//            }else{
//                System.out.println("扣减失败,库存不足");
//            }
//        }finally {
//            redissonLock.unlock();
//        }
//        return "end";
//    }
}

5. 参考连接

Redis的分布式锁详解

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值