分布式锁一步步的推导


2、知道分布式锁吗?有哪些实现方案? 你谈谈对redis分布式锁的理解

在这里插入图片描述

Base案例

1、建Module
在这里插入图片描述

2、改POM

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.sl</groupId>
    <artifactId>boot_redis01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>boot_redis01</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.3.0.RELEASE</spring-boot.version>
    </properties>

    <dependencies>
        <!--默认是lettuce客户端-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- redis依赖commons-pool 这个依赖一定要添加 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.sun.mail</groupId>
            <artifactId>jakarta.mail</artifactId>
            <version>1.6.5</version>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>



        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

3、写YML

server:
  port: 1111

spring:
  redis:
    port: 6379
    host: 192.168.80.16
    lettuce:
      pool:
        max-active: 8 # 连接池最大连接数(使用负值表示没有限制)
        max-idle: 8 # 连接池中的最大空闲连接
        min-idle: 0 # 连接池中的最小空闲连接
        max-wait: 1000 # 连接池最大阻塞等待时间(使用负值表示没有限制)
      shutdown-timeout: 100
    password: root

3、业务类
config

@Configuration
public class RedisConfig {

/**
 * 保证不是序列化后的乱码配置
 */
    @Bean
    public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory){
        RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setConnectionFactory(connectionFactory);
        return redisTemplate;
    }
}

controller

@RestController
public class GoodController {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

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

    @GetMapping("/buy_goods")
    public String buy_Goods(){

        String result = stringRedisTemplate.opsForValue().get("goods:001");
        int goodsNumber = result == null ? 0 : Integer.parseInt(result);

        if (goodsNumber > 0){
            int realNumber = goodsNumber - 1;
            stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
            System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口: "+serverPort);
            return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口: "+serverPort;
        }else {
            System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort);
        }
        return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort;
    }

}

V2.0

没有加锁,并发下数字不对,出现超卖现象,于是加上🔒升级到V2.0。

    public String buy_Goods(){
        synchronized (this) {
            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);

            if (goodsNumber > 0){
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
                System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口: "+serverPort);
                return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口: "+serverPort;
            }else {
                System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort);
            }
            return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort;
        }
    }

在单机环境下,可以使用synchronized或Lock来实现。
但是在分布式系统中,因为竞争的线程可能不在同一个节点上(同一个jvm中),所以需要一个让所有进程都能访问到的锁来实现,比如redis或者zookeeper来构建;
不同进程jvm层面的锁就不管用了,那么可以利用第三方的一个组件,来获取锁,未获取到锁,则阻塞当前想要运行的线程

V3.0

分布式部署后,单机锁还是出现超卖现象,需要分布式锁。用nginx搭建一下负载均衡和反向代理。
在这里插入图片描述

现在访问http://192.168.80.16/buy\_goods,可以点击看到效果,一边一个,默认轮询


现在用jmeter压测一下,可以发现出现了超卖现象。

在这里插入图片描述


解决:上redis分布式锁setnx
Redis具有极高的性能,且其命令对分布式锁支持友好,借助SET命令即可实现加锁处理.


@RestController
public class GoodController {


 public static final String REDIS_LOCK_KEY = "lockhhf";

@Autowired
private StringRedisTemplate stringRedisTemplate;

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

@GetMapping("/buy_goods")
public String buy_Goods(){

  String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
//setIfAbsent() 就是如果不存在就新建
Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value);//setnx

   if (!lockFlag) {  
return "抢锁失败,┭┮﹏┭┮";
}else {
            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);

            if (goodsNumber > 0){
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口: "+serverPort);
 stringRedisTemplate.delete(REDIS_LOCK_KEY);//释放锁
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口: "+serverPort;
}else {
                System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort;
}
    }
}

V4.0

V3.0的问题:出异常的话,可能无法释放锁, 必须要在代码层面finally释放锁

    @GetMapping("/buy_goods")
    public String buy_Goods(){

        String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
        try {
//setIfAbsent() 就是如果不存在就新建
            Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value);//setnx

            if (!lockFlag) {
                return "抢锁失败,┭┮﹏┭┮";
            }else {
                String result = stringRedisTemplate.opsForValue().get("goods:001");
                int goodsNumber = result == null ? 0 : Integer.parseInt(result);

                if (goodsNumber > 0){
                    int realNumber = goodsNumber - 1;
                    stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
                    System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口: "+serverPort);
                    stringRedisTemplate.delete(REDIS_LOCK_KEY);//释放锁
                    return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口: "+serverPort;
                }else {
                    System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort);
                }
                return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort;
            }
        } finally {
                stringRedisTemplate.delete(REDIS_LOCK_KEY);//释放锁
        }
    }

V5.0

V4.0问题:部署了微服务jar包的机器挂了,代码层面根本没有走到finally这块, 没办法保证解锁,这个key没有被删除,需要加入一个过期时间限定key

解决:需要对lockKey有过期时间的设定

在这里插入图片描述

V6.0

V5.0问题:设置key+过期时间分开了,必须要合并成一行具备原子性

解决:将设置key+过期时间和为一行

 //setIfAbsent() == setnx 就是如果不存在就新建,同时加上过期时间保证原子性
 Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value,10L, TimeUnit.SECONDS);

V7.0

V6.0的问题:可能会张冠李戴,删除了别人的锁。

比如A线程进来,拿到锁,锁的有效期为10s,但是A操作了12S,到第10S的时候这个锁会被自动删除,B一看没锁了,也进来了,A操作完了,一看还有一把锁,就给它删除了。其实这是B的锁。

解决:确保来解锁的线程就是持有这个锁的线程
在这里插入图片描述

在这里插入图片描述

V8.0

V7.0的问题:finally块的判断+del删除操作不是原子性的
解决:
①用redis自身的事务:大体思路就是用WATCH监视锁,然后开启事务,去删除锁,如果成功就UNWATCH。否则会一直尝试。
在这里插入图片描述

finally {
            while (true)
            {
                stringRedisTemplate.watch(REDIS_LOCK_KEY); //加事务,乐观锁
                if (value.equalsIgnoreCase(stringRedisTemplate.opsForValue().get(REDIS_LOCK_KEY))){
                    stringRedisTemplate.setEnableTransactionSupport(true);
                    stringRedisTemplate.multi();//开始事务
                    stringRedisTemplate.delete(REDIS_LOCK_KEY);
                    List list = stringRedisTemplate.exec();
                    if (list == null) {  //如果等于null,就是没有删掉,删除失败,再回去while循环那再重新执行删除
                        continue;
                    }
                }
                //如果删除成功,释放监控器,并且breank跳出当前循环
                stringRedisTemplate.unwatch();
                break;
            }
        }

②用lua脚本

V9.0

①要解决Redis分布式锁续期的问题。
②Redis集群+CAP对比zookeeper:
在这里插入图片描述

V10.0

综合上述。redis集群环境下,我们自己写的也不OK, 直接上RedLock之Redisson落地实现。
Config

import org.redisson.Redisson;
import org.redisson.Config;
import org.springframework.beans.factory.annotation.Value;
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;

import java.io.Serializable;

/**
 * 保证不是序列化后的乱码配置
 */
@Configuration
public class RedisConfig {

    @Value("${spring.redis.host}")
    private String redisHost;

    @Bean
    public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory){
        RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setConnectionFactory(connectionFactory);
        return redisTemplate;
    }

    @Bean
    public Redisson redisson(){
        Config config = new Config();
        config.useSingleServer().setAddress(redisHost+":6379").setDatabase(0).setPassword("root");
        return (Redisson) Redisson.create(config);
    }
}

Controller

@RestController
public class GoodController {


public static final String REDIS_LOCK_KEY = "lockhhf";

@Autowired
private StringRedisTemplate stringRedisTemplate;

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

@Autowired
private Redisson redisson;

@GetMapping("/buy_goods")
public String buy_Goods(){

        String value = UUID.randomUUID().toString()+Thread.currentThread().getName();

RLock redissonLock = redisson.getLock(REDIS_LOCK_KEY);
redissonLock.lock();
        try{
                String result = stringRedisTemplate.opsForValue().get("goods:001");
                int goodsNumber = result == null ? 0 : Integer.parseInt(result);

                if (goodsNumber > 0){
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口: "+serverPort);
                    return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口: "+serverPort;
}else {
                    System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort;

}finally {
            redissonLock.unlock();
}
    }
}

V11.0

V10.0的Bug:是在并发多的时候就可能会遇到这种错误,可能会被重新抢占
在这里插入图片描述
解决:
在这里插入图片描述

总结

最开始没加锁,不ok

synchronized 单机版oK,上分布式

nginx分布式微服务 单机锁不行

取消单机锁 上redis分布式锁setnx

只加了锁,没有释放锁, 出异常的话,可能无法释放锁, 必须要在代码层面finally释放锁

宕机了,部署了微服务代码层面根本没有走到finally这块,
没办法保证解锁,这个key没有被删除,需要有lockKey的过期时间设定

为redis的分布式锁key,增加过期时间。
此外,还必须要setnx+过期时间必须同一行的原子性操作

必须规定只能自己删除自己的锁,你不能把别人的锁删除了,防止张冠李戴,1删2的锁,2删3的。

为了解决上一步的问题,在finally块中加了一个if判断再进行删除,但是if和del不是原子性的。所以用lua或者事务来保证原子性

redis集群环境下,我们自己写的也不OK。
直接上RedLock之Redisson落地实现


文章作者: fFee-ops
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 fFee-ops !
评论
  目录