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

news2024/9/23 23:27:26

在分布式系统中,当多个服务实例(或节点)需要访问或修改同一份共享资源时,就需要使用分布式锁来确保数据的一致性和防止并发问题。这种情况下,传统的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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1944375.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

.NET开源、简单、实用的数据库文档生成工具

前言 今天大姚给大家分享一款.NET开源(MIT License)、免费、简单、实用的数据库文档(字典)生成工具,该工具支持CHM、Word、Excel、PDF、Html、XML、Markdown等多文档格式的导出:DBCHM。 支持的数据库 Sq…

Docker+consul容器服务的更新与发现

1、Consul概述 (1)什么是服务注册与发现 服务注册与发现是微服务架构中不可或缺的重要组件。起初服务都是单节点的,不保障高可用性,也不考虑服务的压力承载,服务之间调用单纯的通过接口访问。直到后来出现了多个节点…

初学MySQl简单sql语句(1)

目录 SQL语句介绍: DDL创建数据库: char和varchar比较 数值类型 数据库存储引擎 数据库存储引擎——InnoDB 数据库存储引擎——MyISAM 数据库存储引擎-MyISAM 和InnoDB区别 修改和删除数据库表 数据库设计三大范式 一、什么是范式 二、约束作…

如何批量重命名文件名?批量快速自定义文件名称怎么操作?

如何批量重命名文件名?批量快速自定义文件名称怎么操作?在高效率的信息化时代,呆板的工作方式已经不能满足我们的时效性,很多场景都需要一个高效率的方法来提升我们的工作效率,来完成任务的进度! 如果用手…

redis的持久化机制以及集群模式

1.redis的持久化机制 内存数据库具有高速读写的优势,但由于数据存储在内存中,一旦服务器停止或崩溃,所有数据将会丢失。持久化机制的引入旨在将内存中的数据持久化到磁盘上,从而在服务器重启后能够恢复数据,提供更好的…

Python异常处理机制、调试模式

一、Bug的由来 1、Bug的由来 世界上第一部万用计算机的进化版-马克2号(Mark II) 2、Debug 二、Bug的分类 和 不同异常类型的处理方式 1、粗心导致的语法错误 SyntaxError (1) # age input(请输入你的年龄) # if age > 18: # print(成年人..…

深入浅出mediasoup—通信框架

libuv 是一个跨平台的异步事件驱动库,用于构建高性能和可扩展的网络应用程序。mediasoup 基于 libuv 构建了包括管道、信号和 socket 在内的一整套通信框架,具有单线程、事件驱动和异步的典型特征,是构建高性能 WebRTC 流媒体服务器的重要基础…

华为AR6300S路由器开启SSH远程登录

登录华为路由器: 使用控制台线连接到路由器的控制台端口或者通过Telnet或Web界面远程登录到设备。进入系统视图: 输入system-view(或者简写为sys)命令进入系统视图模式,这是配置全局参数的地方。生成RSA密钥对&#x…

dockerfile部署wordpress

1.将容器直接提交成镜像 [rootlocalhost ~]# docker commit 8ecc7f6b9c12 nginx:1.1 sha256:9a2bb94ba6d8d952527df616febf3fbc8f842b3b9e28b7011b50c743cd7b233b [rootlocalhost ~]# docker images REPOSITORY TAG IMAGE ID CREATED SIZE nginx …

昇思25天学习打卡营第22天 | DCGAN生成漫画头像

昇思25天学习打卡营第22天 | DCGAN生成漫画头像 文章目录 昇思25天学习打卡营第22天 | DCGAN生成漫画头像DCGAN模型数据集数据下载和超参数创建数据集数据集可视化 搭建网络生成器判别器损失函数和优化器 模型训练总结打卡 DCGAN模型 深度卷积对抗生成网络(Depp Co…

【区块链+绿色低碳】巴中市生态价值核算创新应用 | FISCO BCOS应用案例

生态产品总值(GEP),指一定区域生态系统为人类福祉和经济社会可持续发展提供的产品与服务价值总和,包 括供给产品价值、调节服务价值和文化服务价值。当前,推动生态产品价值有效转化存在“难度量、难抵押、难交易、 难变…

nodejs启动项目报错 Error: listen EACCES: permission denied 0.0.0.0:5000

nodejs启动项目报错 Error: listen EACCES: permission denied 0.0.0.0:5000,截图如下: 解决方法 在管理员权限下打开 CMD(命令行)并运行: net stop winnatnet start winnat 执行完成后在此通过nodejs启动项目即可…

【SpringCloud】 微服务分布式环境下的事务问题,seata大合集

目录 微服务分布式环境下的事务问题 分布式事务 本地事务 BASE理论与强弱一致性 BASE理论 强弱一致性 常见分布式事务解决方案 - 2PC 常见分布式事务解决方案 - TCC 常见分布式事务解决方案 - 最大努力通知 常见分布式事务解决方案 - 最终一致性 Seata介绍与术语 Seata…

UE4-字体导入

一.字体导入 方法一: 然后通过导入将自己想要的字体导入到项目中,也可以直接将我们放在桌面的字体直接拖入到我们的内容浏览器中。 但是要注意想要发售游戏的话不可以这样导入微软的字体,因为Windows自带基本都有版权,所以最…

windows10 安装CUDA教程

如何在windows10系统上安装CUDA? 1、查看电脑的NVIDIA版本 nvidia-smi 2、官网下载所需CUDA版本 官网地址:https://developer.nvidia.com/cuda-toolkit-archive 我们所安装的CUDA版本需要小于等于本机电脑的NVIDIA版本。推荐使用迅雷下载,速度会更快哦。 3、安装步骤

【C++高阶】深度剖析:从零开始模拟实现 unordered 的奥秘

📝个人主页🌹:Eternity._ ⏩收录专栏⏪:C “ 登神长阶 ” 🤡往期回顾🤡:哈希底层 🌹🌹期待您的关注 🌹🌹 ❀哈希 📒1. 改造 HashTable…

C++学习笔记04-补充知识点(问题-解答自查版)

前言 以下问题以Q&A形式记录,基本上都是笔者在初学一轮后,掌握不牢或者频繁忘记的点 Q&A的形式有助于学习过程中时刻关注自己的输入与输出关系,也适合做查漏补缺和复盘。 本文对读者可以用作自查,答案在后面&#xff0…

go-kratos 学习笔记(1) 安装

简介: Kratos 一套轻量级 Go 微服务框架,包含大量微服务相关框架及工具。 使用步骤: 安装cli工具 go install github.com/go-kratos/kratos/cmd/kratos/v2latest 创建项目 通过 kratos 命令创建项目模板 # 国内拉取失败可使用gitee源 krat…

C:一些题目

1.分数求和 计算1/1-1/21/3-1/41/5 …… 1/99 - 1/100 的值 #include <stdio.h>int main(){double sum 0.0; // 使用 double 类型来存储结果&#xff0c;以处理可能的小数部分int sign 1; // 符号标志&#xff0c;初始为 1 表示正数for (int i 1; i < 100; i)…

PGSQL学习-基础表结构

1 访问数据库 创建好数据库后&#xff0c;你可以有三种方式访问数据库 运行PostgreSQL的交互式终端程序&#xff0c;它被称为psql&#xff0c; 它允许你交互地输入、编辑和执行SQL命令。 使用一种已有的图形化前端工具&#xff0c;比如pgAdmin或者带ODBC或JDBC支持的办公套件…