32————分布式Lock

创建一个工程
 <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.4.RELEASE</version>
    </parent>
    <dependencies>
        <!--springBoot 相关  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--   将库存 和 订单 都存储到 redis 中 ,简化开发,提高响应速度-->
        <!-- 引入redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
    </dependencies>
​​​​​​配置文件
server.port=8080
spring.redis.host=39.105.6.74
spring.redis.port=6379
​​​​​​Test
package k.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * Test
 * <p>
 * Maxim 大鹏起兮云飞扬
 * Author ZSK
 * Date   2024/5/28 21:03
 */
@RestController
public class Test {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    /**
     * 初始化  库存 订单状态
     * @param goods
     * @return
     */
    @RequestMapping("/init")
    public String init(String goods){
        // 初始化库存和订单
        stringRedisTemplate.opsForValue().set("stack-"+goods,1000+"");
        stringRedisTemplate.opsForValue().set("order-"+goods,0+"");

        return "当前商品-库存"+stringRedisTemplate.opsForValue().get("stack-"+goods) +
                "----订单:"+ stringRedisTemplate.opsForValue().get("order-"+goods);
    }
    /**
     * 获取 库存 订单状态
     * @param goods
     * @return
     */
    @RequestMapping("/state")
    public String state(String goods){
        return "当前商品-库存"+stringRedisTemplate.opsForValue().get("stack-"+goods) +
                "----订单:"+ stringRedisTemplate.opsForValue().get("order-"+goods);
    }
}
  • 在分布式环境下,传统的一些技术无法保证原子性,还有定时任务也可能会出现重复执行的问题。此时需要分布式锁来解决上述问题
  • 为什么锁不住???
    • 锁对象不同,所以锁不住
    • 锁不在同一个Tomcat服务器

分布式锁原理

分布式锁介绍

  • image-20240528194805971

Zookeeper实现分布式锁

Zookeeper实现分布式锁原理

image-20240528195318049

实战

引入Zookeeper依赖
    <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>3.6.0</version>
        </dependency>
        <!--
              zk 本身提供的api,比较复杂。难以使用
              此时我们可以使用zk封装的工具类  curator
        -->
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>4.0.1</version>
        </dependency>
​​​​​​配置Zookeeper
package k.config;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
 * ZkLockConfig
 * <p>
 * Maxim 大鹏起兮云飞扬
 * Author ZSK
 * Date   2024/5/28 17:51
 */
@Configuration
public class ZkLockConfig {
    /**
     * 返回zk 连接
     * @return
     */
    @Bean// 加入到容器中
    public CuratorFramework getConnect(){
        /**
         * 配置zk 断联以后的重试策略   每隔5s 重试一次,最多3次
         */
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(5000,3);

        // .builder() .xxx.build()    建造者模式,用于传递 复杂的多参数
        CuratorFramework curatorFramework = CuratorFrameworkFactory.builder()
                .connectString("39.105.6.74:2181,39.105.6.74:2182,39.105.6.74:2183")
                .retryPolicy(retryPolicy).build();
        curatorFramework.start();// 开始连接
        return  curatorFramework;
    }
}
​​​​​​使用Zookeeper完成分布式锁
package k.controller;

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

/**
 * ZookeeperLockController
 * <p>
 * Maxim 大鹏起兮云飞扬
 * Author ZSK
 * Date   2024/5/28 20:31
 */
@RestController
public class ZookeeperLockController {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private CuratorFramework curatorFramework;

    @RequestMapping("/zk")
    public  String zkKillGoods(String goods) throws Exception {
        //CuratorFramework client, zk 客户端
        // String path //在那个节点下创建临时有序节点
        InterProcessMutex lock = new InterProcessMutex(curatorFramework,"/"+goods);
        // lock.acquire(5, TimeUnit.SECONDS)
        // 尝试获取锁5s ,5s得到锁返回true ,如果没有得到返回false(客户端也会断开连接,节点自动删除)
        if (lock.acquire(5, TimeUnit.SECONDS)){
            // 1.读取库存
            String strStack = stringRedisTemplate.opsForValue().get("stack-" + goods);
            int stack = 0;
            if (strStack != null) {
                stack = Integer.valueOf(strStack);
                if (stack<=0){
                    lock.release();
                    return "抱歉库存不足 ,秒杀完毕";
                }
            }
            // 模拟耗时操作
            Thread.sleep(1);
            // 2.削减库存
            stringRedisTemplate.opsForValue().set("stack-" + goods,(stack-1)+"");
            // 3.创建订单  订单增加+1
            stringRedisTemplate.opsForValue().increment("order-"+goods);
            // 释放锁   删除自己创建的临时有序节点
            lock.release();
            // 4.返回秒杀成功
            return "恭喜你,秒杀成功。";
        }else {
            return "很遗憾抢购失败,请稍后重试";
        }
    }
}
​​​​​​测试初始化商品,启动两个ab工具去秒杀
http://localhost:8080/init?goods=p70

ab.exe -n 5000  -c 10   http://localhost:8080/zk?goods=p70
ab.exe -n 5000  -c 10   http://localhost:8088/zk?goods=p70

http://localhost:8088/state?goods=p70

image-20240528195930684

Redis完成分布式锁

  • 缺点:不可重入,不可以阻塞,setnx和expire分两步执行,非原子操作

Redis实现分布式锁原理

image-20240528200046457

实战

创建redis锁
package k.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * RedisLockConfig
 * <p>
 * Maxim 大鹏起兮云飞扬
 * Author ZSK
 * Date   2024/5/28 19:11
 */
@Component
public class RedisLockConfig {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    /**
     * 获取锁
     */
    public boolean lock(String key){
        //设置国企时间   一定大于业务运行时间
        Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent(key, "0", 5, TimeUnit.SECONDS);
        return absent;
    }
    /**
     * 释放锁
     */
    public void unlock(String key){
        if (stringRedisTemplate.hasKey(key)){
            stringRedisTemplate.delete(key);
        }
    }
}
​​​​​​基于redis 的 setnx完成分布式不锁
package k.controller;

import k.config.RedisLockConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * RedisLockController
 * <p>
 * Maxim 大鹏起兮云飞扬
 * Author ZSK
 * Date   2024/5/28 20:52
 */
@RestController
public class RedisLockController {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private RedisLockConfig redisLockConfig;
    @RequestMapping("/redis")
    public String redis(String goods) throws InterruptedException {
        // 尝试获取锁:trues:得到锁   false:阻塞,无法获取锁
        if (redisLockConfig.lock(goods)) {
            // 读取库存
            String strStack = stringRedisTemplate.opsForValue().get("stack-" + goods);
            int stack = 0;
            if (strStack != null) {
                stack = Integer.valueOf(strStack);
                if (stack < 0) {
                    redisLockConfig.unlock(goods);// 释放锁
                    return "抱歉库存不足,秒杀完毕";
                }
            }
            Thread.sleep(10);//模拟耗时操作
            //削减库存
            stringRedisTemplate.opsForValue().set("stack-" + goods, (stack - 1) + "");
            //创建订单  订单+1
            stringRedisTemplate.opsForValue().increment("order-" + goods);
            // 释放锁   删除自己创建的临时有序节点
            redisLockConfig.unlock(goods);
            return "恭喜你,秒杀成功。";
        } else {
            return "抢购失败,稍后重试";
        }
    }
}

​​​​​​重新启动2个应用初始化订单,并且测试r
http://localhost:8080/init?goods=p70

ab.exe -n 5000  -c 10   http://localhost:8080/redis?goods=p70
ab.exe -n 5000  -c 10   http://localhost:8088/redis?goods=p70

http://localhost:8088/state?goods=p70

image-20240528202101388

Redisson完成分布式锁

  • 本质:setnx+lua脚本

  • 优点:解决Redis的不可重入,不可阻塞,非原子操作的问题

  • 基于NIO的Netty框架的企业级的开源Redis Client,也提供了分布式锁的支持,Redisson 是架设在 Redis 基础上的一个 Java 驻内存数据网格框架, 充分利用 Redis 键值数据库提供的一系列优势, 基于 Java 实用工具包中常用接口, 为使用者提供了 一系列具有分布式特性的常用工具类.

Redis、Redis lua脚本和Redission加锁对比
方案实现原理优点缺点
基于Redis命令1. 加锁:执行setnx,若成功再执行expire添加过期时间2. 解锁:执行delete命令实现简单,相比数据库和分布式系统的实现,该方案最轻,性能最好1.setnx和expire分2步执行,非原子操作;若setnx执行成功,但expire执行失败,就可能出现死锁 2.delete命令存在误删除非当前线程持有的锁的可能 3.不支持阻塞等待、不可重入
基于Redis Lua脚本1. 加锁:执行SET lock_name random_value EX seconds NX 命令2. 解锁:执行Lua脚本,释放锁时验证random_value – ARGV[1]为random_value, KEYS[1]为lock_name if redis.call(“get”, KEYS[1]) == ARGV[1] then return redis.call(“del”,KEYS[1]) else return 0 end实现逻辑上也更严谨,除了单点问题,生产环境采用用这种方案,问题也不大不支持锁重入,不支持阻塞等待
基于Redission结合redis和lua脚本实现支持锁重入、支持阻塞等待、Lua脚本原子操作Redisson 的宗旨是促进使用者对 Redis 的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。

实战

<!-- redisson -->
<dependency>
       <groupId>org.redisson</groupId>
       <artifactId>redisson-spring-boot-starter</artifactId>
       <version>3.16.7</version>
</dependency>
​​​​​​配置redisson 和 redis绑定
	@Bean   //一般与@Configuration配合使用
    public RedissonClient redissonClient(){
        Config config = new Config();
        config.useSingleServer().setAddress("redis://121.41.51.18:6379").setDatabase(0);
        RedissonClient redissonClient = Redisson.create(config);

        return redissonClient;
    }
​​​​​​使用Redisson 完成分布式锁
 @Autowired
    private RedissonClient redissonClient;
    /**
     * 基于redisson 完成分布式锁
     * @param goods
     * @return
     * @throws Exception
     */
    @RequestMapping("/redissonKillGoods")
    public  String redissonKillGoods(String goods) throws Exception {

        // redisson的锁
        RLock lock = redissonClient.getLock(goods);
        //long waitTime, 最大的阻塞等待锁的时间
        // long leaseTime, 最少持有锁的时间
        // TimeUnit unit  时间单位
        if (lock.tryLock(5,5,TimeUnit.SECONDS)){ // 尝试获取锁,返回true,就得到锁,返回fasle 其他线程持有锁  此时无法阻塞获取锁()
            // 1.读取库存
            String strStack = stringRedisTemplate.opsForValue().get("stack-" + goods);
            int stack = 0;
            if (strStack != null) {
                stack = Integer.valueOf(strStack);
                if (stack<=0){
                   lock.unlock();// 释放锁
                    return "抱歉库存不足 ,秒杀完毕";
                }
            }
            // 模拟耗时操作
            Thread.sleep(10);
            // 2.削减库存
            stringRedisTemplate.opsForValue().set("stack-" + goods,(stack-1)+"");
            // 3.创建订单  订单增加+1
            stringRedisTemplate.opsForValue().increment("order-"+goods);
            // 释放锁   删除自己创建的临时有序节点
             lock.unlock();
            // 4.返回秒杀成功
            return "恭喜你,秒杀成功";
        }else {
            return "很遗憾抢购失败,请稍后重试";
        }
    }
​​​​​​测试
http://localhost:8080/init?goods=p70

ab.exe -n 5000  -c 10   http://localhost:8088/redisson?goods=p70
ab.exe -n 5000  -c 10   http://localhost:8080/redisson?goods=p70

http://localhost:8088/state?goods=p70
Redisson原理

源码分析

redisson所有指令都通过lua脚本执行,redis支持lua脚本原子性执行

  • 如果一个key的持有时间是30s,但是执行的业务逻辑超过了30s,此时就有可能锁失效问题,如何解决?

看门狗机制

  • redisson中有一个watchdog的概念,翻译过来就是看门狗,它会在你获取锁之后,每隔10秒帮你把key的超时时间设为30s,这样的话,就算一直持有锁也不会出现key过期了,其他线程获取到锁的问题了。
  • redisson的“看门狗”逻辑保证了没有死锁发生,如果机器宕机了,看门狗也就没了。此时就不会延长key的过期时间,到了30s之后就会自动过期了,其他线程可以获取到锁
  • image-20240529153402945

Redission请求流程图

  • image-20240529153438952
不足点
  • 它加锁时只作用在一个Redis节点上,即使Redis通过sentinel保证高可用,如果这个master节点由于某些原因发生了主从切换,那么就会出现锁丢失的情况
  • 在Redis的master节点上拿到了锁;但是这个加锁的key还没有同步到slave节点;master故障,发生故障转移,slave节点升级为master节点;导致锁丢失。

RedLock完成分布式锁并解决锁丢失问题

本质:封装了多个RedissonLock,只有超过一半以上的RedissonLock得到锁时,才会认为RedLock 得到锁

RedLock解决锁丢失问题

实战

package k.controller;
import org.redisson.RedissonRedLock;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
/**
 * RedLockController
 * <p>
 * Maxim 大鹏起兮云飞扬
 * Author ZSK
 * Date   2024/5/29 15:02
 */
@RestController
public class RedLockController {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private RedissonClient redissonClient;
    //基于Red锁  完成分布式锁
    @RequestMapping("/red")
    public String red(String goods) throws Exception {
        RLock lock1 = redissonClient.getLock(goods + "-lock1");
        RLock lock2 = redissonClient.getLock(goods + "-lock2");
        RLock lock3 = redissonClient.getLock(goods + "-lock3");
        //创建RedLock
        RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
        if (redLock.tryLock(5,5, TimeUnit.SECONDS)){//尝试获取Lock   true:得到锁    false:其他线程持有锁,此时阻塞无法获取锁
            //读取库存
            String strStack = stringRedisTemplate.opsForValue().get("stack-" + goods);
            int stack = 0;
            if (strStack!=null){
                stack= Integer.valueOf(strStack);
                if (stack<=0){
                    redLock.unlock();//释放锁
                    return "库存不足,秒杀失败";
                }
            }
            // 模拟耗时操作
            Thread.sleep(10);
            // 2.削减库存
            stringRedisTemplate.opsForValue().set("stack-" + goods,(stack-1)+"");
            // 3.创建订单  订单增加+1
            stringRedisTemplate.opsForValue().increment("order-"+goods);
            // 释放锁   删除自己创建的临时有序节点
            redLock.unlock();
            // 4.返回秒杀成功
            return "恭喜你,秒杀成功";
        }else{
            return "抢购失败,青山后重试";
        }
    }
}
http://localhost:8080/init?goods=p70

ab.exe -n 5000  -c 10   http://localhost:8080/red?goods=p70
ab.exe -n 5000  -c 10   http://localhost:8088/red?goods=p70

http://localhost:8088/state?goods=p70
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值