首页 > 数据库 >Redis 分布式锁

Redis 分布式锁

时间:2022-08-28 15:33:56浏览次数:42  
标签:String realStock opsForValue Redis 分布式 stringRedisTemplate localKey stock


概述

单机架构下,一个进程中的多个线程竞争同一共享资源时,通常使用 JVM 级别的锁即可保证互斥,以对商品下单并扣库存为例:

public String deductStock() {
    synchronized (this){
        // 获取库存值
        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("扣减失败,库存不足");
        }
    }
    return "end";
}

然而,当使用分布式架构时,这种方式就不管用了,因为 JVM 锁只能控制自家应用,其他机器的应用时管不了的,这时候分布式锁就派上用场了,它能保证分布式系统下不同进程对共享资源访问的互斥性


案例分析

下面对使用 Redis 实现分布式锁的案例进行分析:

1. Case1

使用 Redis 中的 setnx() 设计一个入门级别的分布式锁

public String deductStock1() {
    String localKey = "lock:product:0001";
    Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(localKey, "true");
    if (!aBoolean){
        return "当前系统繁忙";
    }
    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 {
        // 即使中间的任何一处逻辑抛出异常,也能保证锁释放
        stringRedisTemplate.delete(localKey);
    }
    return "end";
}

存在的问题:锁没有释放,机器却宕机了,这时候其他机器将无法获取锁

2. Case2

设置一个过期时间,解决 Case1 中存在的宕机没有释放锁的问题

public String deductStock2() {
    String localKey = "lock:product:0001";
    Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(localKey, "true");
    stringRedisTemplate.expire(localKey,10,TimeUnit.SECONDS);
    if (!aBoolean){
        return "当前系统繁忙";
    }
    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 {
        stringRedisTemplate.delete(localKey);
    }
    return "end";
}

存在的问题:有可能还没有执行到 expire() 就宕机了,没有保证原子性

3. Case3

在加锁时就设置超时时间,保证加锁和设置超时时间是原子操作

public String deductStock3() {
    String localKey = "lock:product:0001";
    // 这条命令能够保证原子性
    Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(localKey, "true",10,TimeUnit.SECONDS);
    if (!aBoolean){
        return "当前系统繁忙";
    }
    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 {
        stringRedisTemplate.delete(localKey);
    }
    return "end";
}

存在问题:如果系统并发量不是特别的大,那么问题不大,但如果并发量很大,就会出现严重的并发问题:

  • 假设线程 A 的时间超过了超时时间,锁失效了,此时该线程 A 还没有执行 delete 方法
  • 线程 B 这时候加锁成功了,与此同时线程 A 执行了 delete 方法,但是这时候线程 A 释放的锁是线程 B 的
  • 于是极端情况下就会出现:线程 A 释放线程 B 的锁,B 释放 C 的,C 释放 D 的 ......

4. Case4

Case3 存在的问题的根本原因就是在执行 delete 方法的时候,自己的锁被其他的线程释放了,所以解决办法就是给每个线程生成一个唯一 ID,在最后释放锁的时候判断是否是自己的锁,如果是自己的才释放

public String deductStock4() {
    String localKey = "lock:product:0001";
    String uuid = UUID.randomUUID().toString();
    // 这条命令能够保证原子性
    Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(localKey, uuid,10,TimeUnit.SECONDS);
    if (!aBoolean){
        return "当前系统繁忙";
    }
    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 (uuid.equals(stringRedisTemplate.opsForValue().get(localKey))){
            stringRedisTemplate.delete(localKey);
        }
    }
    return "end";
}

存在问题:存在原子性问题,问题代码如下:

if (uuid.equals(stringRedisTemplate.opsForValue().get(localKey))){
    stringRedisTemplate.delete(localKey);
}

有可能出现当前线程执行完 if 判断却还没执行 delete 操作的时候当前锁过期了,于是又会出现当前线程释放了其他线程的锁的情况

5. Case5

对于 Case4 的问题,本质是 「判断是不是当前线程加的锁」和「释放锁」不是一个原子操作,可以用 Lua 脚本代替,Redis 会将整个脚本作为一个整体执行

String redisScript = "
    if redis.call('get',KEYS[1]) == ARGV[1] then 
        return redis.call('del',KEYS[1]) 
    else
        return 0
        end;"

public String deductStock5() {
    String localKey = "lock:product:0001";
    String uuid = UUID.randomUUID().toString();
    // 这条命令能够保证原子性
    Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(localKey, uuid,10,TimeUnit.SECONDS);
    if (!aBoolean){
        return "当前系统繁忙";
    }
    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 {
        redisTemplate.execute(redisScript, Arrays.asList(localKey), uuid);
    }
    return "end";
}

也可以使用锁续命的方式解决,即创建一个守护线程,每过一段时间,判断业务的主线程有没有结束(是否还加着锁),如果还加着锁,将锁的超时时间重新设置

public String deductStock5() {
    String localKey = "lock:product:0001";
    String uuid = UUID.randomUUID().toString();
    // 这条命令能够保证原子性
    Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(localKey, uuid,10,TimeUnit.SECONDS);
    if (!aBoolean){
        return "当前系统繁忙";
    } else {
        // 续命
        Thread demo = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    Boolean expire = redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
                    // 有可能已经主动删除key,不需要在续命
                    if(!expire){
                        return;
                    }
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        demo.setDaemon(true);
        demo.start();
    }
    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 (uuid.equals(stringRedisTemplate.opsForValue().get(localKey))){
            stringRedisTemplate.delete(localKey);
        }
    }
    return "end";
}

标签:String,realStock,opsForValue,Redis,分布式,stringRedisTemplate,localKey,stock
From: https://www.cnblogs.com/Yee-Q/p/16632845.html

相关文章

  • Nginx分布式框架详解46-56nginx静态资源部署02
    error_page指令error_page指令是设置网站的错误页面。语法默认值位置error_page......[=[response]];—http、server、location......code是响应......
  • redis主从数据同步原理
    what:redis高可用:1、数据尽量不丢失;2、尽可能的提供服务;栗子:AOF和RDB保证了数据持久化尽量不丢失;主从复制就是增加副本,一......
  • REDIS-读书笔记
    1Redis的概念:Redis是一种key-value类型的内存数据库,可以用于保存string,list,set,sortedset,hash等多种数据结构。由于整个数据库统统加载在内存中进行操作,所以性能也非常出......
  • Redis
    1、redis是一个高性能的key-value数据库2、数据类型Strings,Lists,Hashes,Sets及OrderedSets数据类型操作3、原子性Redis的所有操作都是原子性......
  • redis基础
    NoSQL概述数据存储瓶颈是什么数据量总大小,一个机器放不下数据索引一个机器内存放不下访问量一个服务器不能承受优化数据结构–文件缓存IO后来,随着访问量的上升,几乎大......
  • 分布式系统~常见的 配置中心
    一、disconf二、zookepper三、apollo 四、xxl-conf五、redis六、dimond七、SpringCloudConfig八、SpringCloudAlibabaNacos ......
  • Go语言实现分布式对象存储系统
    实现一个可扩展的,简易的,分布式对象存储系统存储系统介绍先谈谈传统的网络存储,传统的网络存储主要分为两类:NAS,即NewtworkAttachedStorage,是一个提供了存储功能和文件......
  • 启动HDFS伪分布式环境时报权限错误
    问题描述操作系统:Ubuntu18.04LTSHDFS版本:hadoop-3.2.3普通用户登录,参照官方文档在单机上安装伪分布式环境时,启动HDFS报权限错误。具体报错信息如下:$./sbin/start-df......
  • Nginx分布式框架详解-基础37-45nginx静态资源部署01
    nginx静态资源概述上网去搜索访问资源对于我们来说并不陌生,通过浏览器发送一个HTTP请求实现从客户端发送请求到服务器端获取所需要内容后并把内容回显展示在页面的一个......
  • Redis 集群模式
    概述Redis在3.0之后开始支持Cluster(集群)模式,特点如下:支持节点的自动发现:可向集群动态添加节点,并自动融入支持slave-master选举和容错:多个master宕机后,选举出......