Bootstrap

【分布式锁】Redis实现分布式锁

在分布式系统中,当多个服务实例(或节点)需要访问或修改同一份共享资源时,就需要使用分布式锁来确保数据的一致性和防止并发问题。这种情况下,传统的Java并发控制机制如ReentrantLock或synchronized就无法满足需求,因为它们是JVM级别的锁,只能保证单个JVM实例内部的线程同步,而无法跨JVM实例进行同步。

一、一些需要使用分布式锁而无法使用ReentrantLock或synchronized的场景:

  • 跨JVM的资源共享:在分布式系统中,服务通常被部署在多个JVM实例上,这些实例可能分布在不同的物理机器或容器上。当这些实例需要访问或修改同一个数据库记录、缓存项或任何形式的共享资源时,就需要使用分布式锁来确保数据的一致性和操作的原子性。

  • 微服务架构中的分布式事务:在微服务架构中,服务之间通常通过轻量级的通信协议(如HTTP REST API)进行交互。由于服务是独立部署和运行的,因此它们之间的数据一致性需要额外的机制来保证。在某些情况下,可能需要在多个服务之间协调事务操作,这时就需要使用分布式锁来确保事务的完整性和一致性。

  • 分布式缓存的一致性:在使用分布式缓存(如Redis、Memcached等)时,多个服务实例可能会同时读取或写入同一个缓存项。为了确保缓存数据的一致性和减少缓存击穿、雪崩等问题的发生,可能需要使用分布式锁来控制对缓存的并发访问。

  • 分布式文件系统的同步:在分布式文件系统中,多个节点可能同时需要访问或修改同一个文件或目录。为了防止数据冲突和保证文件系统的一致性,需要使用分布式锁来协调各个节点之间的访问操作。

  • 定时任务或作业的分发:在分布式系统中,可能需要运行一些定时任务或批处理作业。为了防止多个节点同时执行同一个任务而导致数据重复处理或资源争用,可以使用分布式锁来确保任务的分发和执行是唯一的。

分布式锁提供了一种跨JVM实例的同步机制,可以确保在分布式环境中对共享资源的访问是安全和一致的。实现分布式锁有多种方式,包括使用数据库、Redis、Zookeeper等中间件来提供锁服务。选择合适的分布式锁实现取决于具体的应用场景、性能要求和系统架构。

接下来我们通过Redis来实现分布式锁。
在实现分布式锁之前,我们需要考虑一个问题,分布式锁是对共享资源或者数据的安全保护,确保数据的一致性和防止并发问题。那同时间内对某个接口的并发请求,怎么模拟呢 ?我这里采用的Nginx来实现。

二、Nginx实现反向代理、负载均衡

在这里插入图片描述

1.官网上下载nginx

具体的安装过程和全部配置 可见博客:【nginx】nginx的配置文件到底是什么结构,到底怎么写?

2.配置负载均衡。

在这里插入图片描述

3.具体配置如下:

在这里插入图片描述

4.主要配置如下:


http {
	upstream backend {
		server 127.0.0.1:8023;
		server 127.0.0.1:8021;
		}
    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    server {
        listen       80;
        server_name  localhost;
		location / {
			proxy_pass http://backend;
			proxy_set_header Host $host;
			proxy_set_header X-Real-IP $remote_addr;
			proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; ##反向代理执行定义的upstream名字
		}

        #charset koi8-r;

...................

三、Redis实现分布式锁

场景:
我分别在cloud项目的不同模块,书写了可以处理这个请求的代码,模拟部署在不同的环境上。以上的Nginx配置将会以轮训的方式进行服务调用。
结构如下:
模块1:provider-and-consumer ,端口:8023
在这里插入图片描述
模块2: rabbitmq-consumer 端口8021
在这里插入图片描述
模块1: providerconsumer 端口 8023
RedisTestController.java如下:

package com.atguigu.gulimall.providerconsumer.controller;

import lombok.extern.slf4j.Slf4j;
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.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

/**
 * @author: jd
 * @create: 2024-07-08
 */
@RestController
@RequestMapping("/test")
@Slf4j
public class RedisTestController {


    @Autowired
    private StringRedisTemplate stringRedisTemplate;


    /**
     * 分布式锁使用
     */

    @GetMapping("/lock")
    public  String deductStock() {

        //获取一个固定的商品ID,作为我们被秒杀的商品ID  这个代表是一种商品,加锁的时候对这个商品加锁,
        // (接着上面)只有持有这个商品redis锁的,才能对这个商品的库存进行扣减,否则加锁不成功的,代表当前的商品的库存正在被另外一个请求进行扣减,当前的这个扣减失败。需要重新下单
        String lockKey="lock:product:101";
        //uuid,防止删除其他人加的锁
        String clientId = UUID.randomUUID().toString();
        //进行加锁,设置过期时间为10s 注意代码的原子性,虽然设置了锁的过期时间是10s,但是仍然存在一个问题,如果我业务还没执行完,锁失效了怎么办 ?此时正在执行的业务中比如进行扣减库存等,会导致重复扣减,超卖等问题。
        Boolean result =stringRedisTemplate.opsForValue().setIfAbsent(lockKey,clientId,10, TimeUnit.SECONDS);
        //如果加锁失败,返回错误,秒未成功
        if(result){
            System.out.println("=====>8023  对 产品ID lock:product:101 分布式锁加锁成功 ,锁值= " + stringRedisTemplate.opsForValue().get(lockKey)+"时间:"+System.currentTimeMillis()+" 即将扣减库存");
        }else {
            System.out.println("=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 ");
            return "";
        }

//        if(!result){
//            return "error_code";
//        }

        try {
            // 获取当前库存
            String stock1 = stringRedisTemplate.opsForValue().get("stock");
            if (stock1 == null) {
                System.out.println("秒杀未开始");
                return "end";  //如果遇到这种直接返回的情况,最初也加上锁了,在最后的finally中也会释放锁,所以不会产生死锁。导致无法扣减库存
            }
            int stock = Integer.parseInt(stock1);
            System.out.println("====>拿到商品库存,库存数量 = " + stock);
            if (stock > 0) {
                // 扣减库存
                int realStock = stock - 1;
                // 更新库存
                Thread.sleep(1); //为了不去掉下面的InterruptedException 这个捕捉,所以直接睡了1毫秒
                System.out.println("*****线程睡眠,模仿业务花费的时间1毫秒"); 
                stringRedisTemplate.opsForValue().set("stock", realStock + ""); //扣减完库存重新的设置上当前处理商品的现有库存数量。
                System.out.println("扣减成功,剩余的库存为:" + realStock);

            } else {
                System.out.println("扣减失败,库存不足");
            }

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //如果是自己加的锁就自己删掉,防止死锁
            if(clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))){
                System.out.println("=====>8023释放锁成功" );
                stringRedisTemplate.delete(lockKey);
            }
        }

        return "end";

    }
    }

模块2代码:
RedisTestController.java代码

package com.atguigu.gulimall.rabbitmqconsumer.controller;

import lombok.extern.slf4j.Slf4j;
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.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

/**
 *
 * 和provider-and-consumer 这两个服务中都有这个RedisTestController,用来模拟两个不同的服务
 * @author: jd
 * @create: 2024-07-08
 */
@RestController
@RequestMapping("/test")
@Slf4j
public class RedisTestController {


    @Autowired
    private StringRedisTemplate stringRedisTemplate;


    /**
     * 分布式锁使用
     */

    @GetMapping("/lock")
    public  String deductStock() {

        //获取一个固定的商品ID,作为我们被秒杀的商品ID  这个代表是一种商品,加锁的时候对这个商品加锁,
        // (接着上面)只有持有这个商品redis锁的,才能对这个商品的库存进行扣减,否则加锁不成功的,代表当前的商品的库存正在被另外一个请求进行扣减,当前的这个扣减失败。需要重新下单
        String lockKey="lock:product:101";
        //uuid,防止删除其他人加的锁
        String clientId = UUID.randomUUID().toString(); //锁的value值是随机的。
        //进行加锁,设置过期时间为10s 注意代码的原子性(这个原子性是指,加上锁的动作和为这个锁设置过期时间的动作保证是一个原子),虽然设置了锁的过期时间是10s,但是仍然存在一个问题,如果我业务还没执行完,锁失效了怎么办 ?此时正在执行的业务中比如进行扣减库存等,会导致重复扣减,超卖等问题。
        Boolean result =stringRedisTemplate.opsForValue().setIfAbsent(lockKey,clientId,10, TimeUnit.SECONDS);
        if(result){
            System.out.println("=====>8021  对 产品ID lock:product:101 分布式锁加锁成功 ,锁值=" + stringRedisTemplate.opsForValue().get(lockKey)+"时间:"+System.currentTimeMillis()+"  即将扣减库存");
        }else {
            System.out.println("=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 ");
            return "";
        }

        /*//如果加锁失败,返回错误,秒未成功
        if(!result){
            return "error_code";
        }*/

        try {
            // 获取当前库存
            String stock1 = stringRedisTemplate.opsForValue().get("stock");
            if (stock1 == null) {
                System.out.println("秒杀未开始");
                return "end";  //如果遇到这种直接返回的情况,最初也加上锁了,在最后的finally中也会释放锁,所以不会产生死锁。导致无法扣减库存
            }
            int stock = Integer.parseInt(stock1);
            System.out.println("=====>拿到商品库存,库存数量 = " + stock);
            if (stock > 0) {
                // 扣减库存
                int realStock = stock - 1;
                // 更新库存
                Thread.sleep(1); //1ms相当于没休眠。 为了不去掉下面的InterruptedException 这个捕捉,所以直接睡了1毫秒
                // Thread.sleep(20000); //这个是为了模拟 网络不好的情况下,花费的时间20s
                System.out.println("*****线程睡眠,模仿业务花费的时间1毫秒");
                stringRedisTemplate.opsForValue().set("stock", realStock + ""); //扣减完库存重新的设置上当前处理商品的现有库存数量。
                System.out.println("扣减成功,剩余的库存为:" + realStock);

            } else {
                System.out.println("扣减失败,库存不足");
            }

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //如果是自己加的锁就自己删掉,防止死锁
            if(clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))){
                System.out.println("======>8021释放锁成功" );
                stringRedisTemplate.delete(lockKey);
            }
        }

        return "end";

    }
    }

代码重点解释
1.使用setnx
SETNX 是 Redis 数据库中的一个命令,用于将一个键值对(key-value pair)设置到 Redis 中,但只有在该键不存在的情况下才会设置成功。如果该键已经存在,SETNX 命令不会对其进行任何操作,并返回 0,否则返回 1。
2.死锁问题
使用setnx进行加锁的时候,一定要设置锁的过期时间。业务完成之后,一定要及时释放锁,避免产生死锁问题。并且一定要保证加锁和设置锁的过期时间操作是原子的,避免只上锁,未设置过期时间问题的存在
3.锁续命问题
上述代码作为一个简单的分布式锁实现,在并发量不算很高的情况下,不会出现什么问题,但是它实际上还是有瑕疵的。我们上述代码,锁失效有两种可能。一种是过期,另一种是代码删除。代码删除没什么问题,我们选择将锁删除的时候,肯定是业务代码执行完毕。但是如果是过期的话,有可能我们的业务代码还没有执行完,锁先过期了,并发量大的情况下,外部不断有请求试图加锁,可能会造成锁失效的情况。

在没有添加分布式锁之前,两个服务器8021 和 8023会出现,超卖现象,具体超卖含义是什么我就不解释了。
超卖现象:
在这里插入图片描述
我们发现,这两个服务虽然单独的看,销售的商品都是正确的,但是放在一起看,就会发现有相同的库存,这就说明,同一个库存被卖了两次,我们上文提到的超卖问题仍然存在!

下面对超卖现象进行解决,介绍一下解决过程,实现分布式锁之后的库存增减效果:

首先设置上100的库存数据(在redis缓存中)
在这里插入图片描述

发送并发压测请求 http://localhost:80/test/lock
可看到我们访问的是80端口,并不是8021 和 8023,这里就成功的应用到了nginx的动态代理转发。

我先用下普通的测试:
访问两次的测试结果(两个服务器轮询处理
第一次访问:
在这里插入图片描述
第二次访问:
在这里插入图片描述
而且可以很明确的看到,每一次获取的所的值是不一样的。

测试结果:
在这里插入图片描述
8023模块处理日志:

=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁成功 ,锁值= a849bddd-6b78-4bd5-9fc1-dba39deaee2a时间:1721698799461 即将扣减库存
====>拿到商品库存,库存数量 = 89
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
*****线程睡眠,模仿业务花费的时间1毫秒
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
扣减成功,剩余的库存为:88
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023释放锁成功
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 

8021模块处理日志:

=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁成功 ,锁值=db91e932-9325-4521-8d19-0d6f4b1794fb时间:1721698799443  即将扣减库存
=====>拿到商品库存,库存数量 = 90
*****线程睡眠,模仿业务花费的时间1毫秒
扣减成功,剩余的库存为:89
======>8021释放锁成功
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁成功 ,锁值=7dbab45b-bff9-485d-9a5a-1fed1f12ac06时间:1721698799480  即将扣减库存
=====>拿到商品库存,库存数量 = 88
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
*****线程睡眠,模仿业务花费的时间1毫秒
扣减成功,剩余的库存为:87
======>8021释放锁成功
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁成功 ,锁值=81441999-f474-4d73-8d45-9f57d1d52b27时间:1721698799493  即将扣减库存
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>拿到商品库存,库存数量 = 87
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
*****线程睡眠,模仿业务花费的时间1毫秒
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
扣减成功,剩余的库存为:86
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 
======>8021释放锁成功
=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 

可以看到两个服务器,一共只有4次扣减商品成功,其余的96次均获取锁失败,导致扣减库存失败,提示用户重新操作。
实现了分布式锁的效果,而且不会出现超卖的情况。

四、实现分布式锁中 几个需要注意的地方::

注意点1:防止死锁

问题:死锁

设置分布式锁的时候,需要给锁设置上锁的过期时间,假设线程1通过SETNX获取到锁并且正常执行然后释放锁那么一切ok,其它线程也能获取到锁。但是线程1现在"耍脾气"了,线程1抱怨说"工作太久有点累需要休息一下,你们想要获取锁等着吧,等我把活干完你们再来获取锁"。此时其它线程就无法向下继续执行,因为锁在线程1手中。这种长期不释放锁情况就有可能造成死锁。

需要注意的是,千万不能把“获取锁”和“设置超时时间”在代码中分成两步执行
在这里插入图片描述
原因在于这两个步骤分开执行没有保证原子性,拿锁到设置过期时间之间是存在时间差的,如果在这之间机器宕机了还是会存在上述问题,解决办法就是在占锁的同时设置过期时间。

解决办法:

为了防止像线程1这种"耍脾气"的现象发生,我们可以设置key的过期时间来解决。设置过期时间过后其它线程可不会惯着线程1,其它线程表示你要休息可以,休息了指定时间把锁让出来然后拍拍屁股走人,没人惯着你。

但是这个过期时间的设置也是需要根据实际的业务进行评估,不能随便设置,因为可能会出现这样的问题,在上述的代码中可以模拟出来,在扣减库存之前,8023模块睡眠Thread.sleep(1000); 1秒,模拟实际业务花费,8021模块睡眠20s,模拟业务消耗。如果我只是设置过期时间是10s的话,那如果请求轮训到8021模块了,等到锁过期了,还没睡醒,此时当前执行的任务就没有锁了,其他的任务就可以重新持有锁了,此时等到8021中的锁过期的线程执行完任务(睡醒了)之后,他会删除锁,如果不判断是谁的锁,是不是他自己的锁,就会产生误删的情况,所以这就引申出了两个需要考虑的点,第一个:过期时间设置的考虑,第二个:删除锁之前需要判断是否是自己的锁!
这两个问题在这个里面都有提到解决办法,希望能帮到大家☺

注意点2: 误删情况

问题:误删情况情况一:

设置过期时间线程1被治得服服帖帖,此时线程1又开始不当人了。线程1想既然你抢我得锁,等你获得锁后我就将锁删除毕竟我还要有备用钥匙,让你也锁不住,让其它线程也执行。 线程1休息的时间超过了过期时间,此时锁会自动释放。线程2现在脱颖而出抢到了锁然后开心的继续执行。但是现在线程1醒了,发现线程2抢走了锁。线程1表示小子胆挺肥啊,敢抢我的锁,等我执行完了就将你锁删除,让其它"哥们"也进来。此时就会发生蝴蝶效应,线程1删除了线程2的锁,线程2删除了线程3的锁,直到最后一个"哥们:wc,我锁了?"。当然线程是无感知,其实线程1乃至其它线程都不知道删除的是别人的锁,全部线程都以为删除的是自己的锁。直到最后一个线程无锁可删。 这种误删锁的情况让锁的存在荡然无存,本来应该串行执行的线程,在一定程度上都开始并发执行了。 那么误删情况该如何解决了?

解决办法:

我们可以给锁加上线程标识,只有锁是当前线程的才能删除,否则不能删除。在添加key的时候,key的value存储当前线程的标识,这个标识只要保证唯一即可。可以使用UUID或者一个自增数据。在删除锁的时候,将线程标识取出来进行判断,如果相同就表示锁是自己的能够删除,否则不能删除。

我的解决办法:
最后删除锁的时候,我这里使用了先判断是否是当前处理的服务的本次处理设置的分布式锁,如果是,才删除,否则不让删除其他线程的服务处理产生的锁,这里一定需要注意,否则会产生锁误删的情况,会让分布式锁失效!

我最初的写法是 (这个是会导致误删):

finally {
            //删除掉锁,防止死锁
                System.out.println("=====>8023释放锁成功" );
                stringRedisTemplate.delete(lockKey);
        }

新的实现方式(这个可以避免误删):

finally {
            //如果是自己加的锁就自己删掉,防止死锁
            if(clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))){
                System.out.println("=====>8023释放锁成功" );
                stringRedisTemplate.delete(lockKey);
            }
        }
还有一种解决办法:

获取锁

//获取线程前缀,同时也是线程表示。通过UUID唯一性
private static final String ID_PREFIX= UUID.randomUUID().toString(true)+"-";
//与线程id组合
public boolean tryLock(long timeOut) {
        //获取线程id
        String id =ID_PREFIX+ Thread.currentThread().getId();
        //获取锁
        Boolean absent = redisTemplate.opsForValue().setIfAbsent(key, id , timeOut, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(absent);
    }

释放锁

public void unLock() {
		//获取存储的线程标识
        String value = stringRedisTemplate.opsForValue().get(key);
        //当前线程的线程标识
        String id =ID_PREFIX+ Thread.currentThread().getId();
        //线程标识相同则删除否,则不删除
        if (id.equals(value)){
            redisTemplate.delete(key);
        }
    }
问题:误删情况情况二
问题

加入线程标识后,线程一不能随便删除其它线程的锁,但是线程1又开始不当人了。线程1表示判断线程标识和释放锁的操作我可以分开执行,这又不是一个原子性的操作,线程1干完活以后就准备去释放锁,当线程1判断锁是自己的后表示开锁太累了,休息一会在开。此时其它线程就想无所谓,反正过期时间一到锁就会自动释放。但是线程1已经判断了锁是自己的以后就不会执行判断锁的操作(线程1已经执行了if判断,只是没有执行方法体),当线程2获得锁后,线程1仍然能删除线程2的锁。

解锁时,查 - 删 操作是 2 个操作,由两个命令完成,非原子性。
redis底层执行这个setnx不是一个原子操作,而是有两步操作完成的,首先set hello world,然后第二步设置key的过期时间:
expire hello 3,那么如果执行完第一步刚好redis宕机了,此时key一直保存到redis。永远也无法删除了。

解决办法:待定

在redis实现的分布式锁中,这种因为服务器可能存在的宕机导致的误删情况是无法预料到的。

下一篇我们将通过Redisson实现分布式锁。这个在日常中较最为常用。

路漫漫其修远兮,吾必将上下求索~
到此关于Redis实现分布式锁就算告一段落了,如果你认为博主写的不错!写作不易,请点赞、关注、评论给博主一个鼓励吧转~

参考链接:https://blog.csdn.net/zhuge_long/article/details/137347203
参考链接:https://blog.csdn.net/hlzdbk/article/details/129940116 、
参考链接:https://blog.csdn.net/h2503652646/article/details/118977164

;