• redis深度历险 千帆竞发 —— 分布式锁


    分布式应用进行逻辑处理时经常会遇到并发问题
    比如一个操作要修改用户的状态,修改状态需要先读出用户的状态,在内存里进行修改,改完了再存回去。如果这样的操作同时进行了,就会出现并发问题,因为读取和保存状态这两个操作不是原子的。(Wiki 解释:所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch 线程切换。)
    在这里插入图片描述
    这个时候就要使用到分布式锁来限制程序的并发执行。Redis 分布式锁使用非常广泛,它是面试的重要考点之一,很多同学都知道这个知识,也大致知道分布式锁的原理,但是具体到细节的使用上往往并不完全正确。

    1 分布式锁

    分布式锁本质上要实现的目标就是在 Redis 里面占一个“茅坑”,当别的进程也要来占时,发现已经有人蹲在那里了,就只好放弃或者稍后再试。

    1 .1 基于set命令的分布式锁

    1. 加锁:使用setnx进行加锁,当该指令返回1时,说明成功获得锁。
    2. 解锁:当得到锁的线程执行完任务之后,使用del命令释放锁,以便其他线程可以继续执行setnx命令来获得锁。
    127.0.0.1:6379> setnx lock:codehole true
    (integer) 1
    127.0.0.1:6379>  .. do something critical ..
    127.0.0.1:6379>  .. do something critical ..
    127.0.0.1:6379> del lock:codehole
    (integer) 1
    127.0.0.1:6379>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    存在的问题:假设线程获取了锁之后,在执行任务的过程中挂掉,来不及显示地执行del命令释放锁,那么竞争该锁的线程都会执行不了,产生死锁的情况。

    1. 解决方案:设置锁超时时间。

    setnx 的 key 必须设置一个超时时间,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放。可以使用expire命令设置锁超时时间。

    127.0.0.1:6379> setnx lock:codehole true
    (integer) 1
    127.0.0.1:6379> expire lock:codehole 5
    (integer) 1
    127.0.0.1:6379>.. do something critical ..
    127.0.0.1:6379>.. do something critical ..
    127.0.0.1:6379> del lock:codehole
    (integer) 1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    存在问题:setnx 和 expire 不是原子性的操作,假设某个线程执行setnx 命令,成功获得了锁,但是还没来得及执行expire 命令,服务器就挂掉了,这样一来,这把锁就没有设置过期时间了,变成了死锁,别的线程再也没有办法获得锁了。

    1. 解决方案:redis的set命令支持在获取锁的同时设置key的过期时间。
    set  lock:codehole    true                ex 5                nx
    SET  lock_name        my_random_value     EX 30000            NX 
    
    • 1
    • 2

    存在问题:

    • 假如线程A成功得到了锁,并且设置的超时时间是 30 秒。如果某些原因导致线程 A 执行的很慢,过了 30秒都没执行完,这时候锁过期自动释放,线程 B 得到了锁。
    • 随后,线程A执行完任务,接着执行del指令来释放锁。但这时候线程 B 还没执行完,线程A实际上删除的是线程B加的锁。

    解决方案:

    可以在 del 释放锁之前做一个判断,验证当前的锁是不是自己加的锁。在加锁的时候把当前的线程 ID 当做value,并在删除之前验证 key 对应的 value 是不是自己线程的 ID。

    但是,这样做其实隐含了一个新的问题,get操作、判断和释放锁是两个独立操作,不是原子性。对于非原子性的问题,我们可以使用Lua脚本来确保操作的原子性。

    1. 锁续期:(这种机制类似于redisson的看门狗机制,文章后面会详细说明)

    虽然步骤4避免了线程A误删掉key的情况,但是同一时间有 A,B 两个线程在访问代码块,仍然是不完美的。怎么办呢?我们可以让获得锁的线程开启一个守护线程,用来给快要过期的锁“续期”。

    线程A执行完需要30秒的时间 :

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

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

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

    1.2 Redisson的基本使用方法

    pom.xml文件的导入依赖:

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
            <dependency>
                <groupId>org.redisson</groupId>
                <artifactId>redisson</artifactId>
                <version>3.15.0</version>
            </dependency>
            <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
            <dependency>
                <groupId>redis.clients</groupId>
                <artifactId>jedis</artifactId>
                <version>3.6.0</version>
            </dependency>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    application.xml:

    spring.redis.port=6379
    spring.redis.host=localhost
    server.port=8081
    
    
    • 1
    • 2
    • 3
    • 4

    启动类:

    package com.zs;
    
    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 SpringbootRedisApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(SpringbootRedisApplication.class, args);
        }
    
        @Bean
        public Redisson redisson() {
            // 单机模式
            Config config = new Config();
            config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);
            return (Redisson) Redisson.create(config);
        }
    
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    测试类:

    package com.zs.controller;
    
    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.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * @author zhaoshuai06 
     * Created on 2021-06-07
     */
    @RestController
    public class IndexController {
        @Autowired
        private Redisson redisson;
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
        @RequestMapping("/deduct_stock")
        public String deductStock() {
            String lockKey = "product_001";
            RLock redissonLock = redisson.getLock(lockKey);
            try{
                redissonLock.lock();
                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("扣减失败, 库存不足:");
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                redissonLock.unlock();
            }
            return "end";
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
  • 相关阅读:
    转载)word输出高分辨PDF并且有链接跳转功能
    《富爸爸财务自由之路》阅读笔记
    java毕业设计班费收支管理系统mybatis+源码+调试部署+系统+数据库+lw
    算法补天系列之中级提高班1
    xpath报错注入
    为什么我设置了DHCP,无法给eth1分配IP地址啊
    八、ChatGPT能替代什么人?
    python毕业设计作品基于django框架 疫苗预约系统毕设成品(7)中期检查报告
    【目标检测】目标尺度和图像分辨率之间的关系
    C语言--每日五道选择题--Day12
  • 原文地址:https://blog.csdn.net/zs18753479279/article/details/132885595