.NET CORE使用Redis分布式锁续命(续期)问题

news/2024/5/20 0:07:58 标签: .netcore, redis, 分布式, 架构

结合上一期 .NET CORE 分布式事务(三) DTM实现Saga及高并发下的解决方案(.NET CORE 分布式事务(三) DTM实现Saga及高并发下的解决方案-CSDN博客)。有的小伙伴私信说如果锁内锁定的程序或者资源未在上锁时间内执行完,造成的使用资源冲突,需要如何解决。本来打算之后在发博文说明这个问题。那就先简短的说明一下。

这是一个Redis分布式锁续命或者称之为续期的问题。废话不多说,直接上代码。

using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Redlock.CSharp;
using StackExchange.Redis;
using System.Diagnostics;
using System.Globalization;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Threading;

public class RedisService
{
    private readonly ConnectionMultiplexer _redis;
    private readonly IDatabase _database;

    /// <summary>
    /// 初始化 <see cref="RedisService"/> 类的新实例。
    /// </summary>
    /// <param name="connectionMultiplexer">连接多路复用器。</param>
    public RedisService(string connectionString)
    {
        _redis = ConnectionMultiplexer.Connect(connectionString);
        _database = _redis.GetDatabase();
    }

    #region 分布式锁

    #region 阻塞锁

    /// <summary>
    /// 阻塞锁--加锁
    /// </summary>
    /// <param name="key">阻塞锁的键</param>
    /// <param name="expireSeconds">阻塞锁的缓存时间</param>
    /// <param name="timeout">加锁超时时间</param>
    /// <returns></returns>
    public bool AcquireLock(string key, int expireSeconds, int timeout)
    {
        var script = @"local isNX = redis.call('SETNX', KEYS[1], ARGV[1])
                           if isNX == 1 then
                               redis.call('PEXPIRE', KEYS[1], ARGV[2])
                               return 1
                           end
                           return 0";
        RedisKey[] scriptkey = { key };
        RedisValue[] scriptvalues = { key, expireSeconds * 1000 };
        var stopwatch = Stopwatch.StartNew();
        while (stopwatch.Elapsed.TotalSeconds < timeout)
        {
            if (_database.ScriptEvaluate(script, scriptkey, scriptvalues).ToString() == "1")
            {
                stopwatch.Stop();
                return true;
            }
        }
        Console.WriteLine($"[{DateTime.Now}]{key}--阻塞锁超时");
        stopwatch.Stop();
        return false;
    }

    Action<string, int, int, IDatabase> postponeAction = (string key, int expireSeconds, int postponetime, IDatabase database) =>
    {
        var stopwatchpostpone = Stopwatch.StartNew();
        while (true)
        {
            //记录时钟大于锁的设置时间说明这个锁已经自动释放了,没必要再用lua脚本去判断了,直接提前退出
            if (stopwatchpostpone.Elapsed.TotalSeconds > expireSeconds) return;
            //提前三分之一时间续命,必须提前。要不真释放了
            if (stopwatchpostpone.Elapsed.TotalSeconds > expireSeconds * 0.66)
            {
                var scriptpostpone = @"local isNX = redis.call('EXISTS', KEYS[1])
                                                       if isNX == 1 then
                                                          redis.call('PEXPIRE', KEYS[1], ARGV[2])
                                                       return 1
                                                          end
                                                       return 0";
                RedisKey[] scriptkey = { key };
                RedisValue[] scriptvalues = { key, postponetime * 1000 };
                if (database.ScriptEvaluate(scriptpostpone, scriptkey, scriptvalues).ToString() == "1")
                    Console.WriteLine($"[{DateTime.Now}]{key}--阻塞锁续命成功");
                else
                    Console.WriteLine($"[{DateTime.Now}]{key}--阻塞锁续命失败");
                return;
            }
        }
    };

    /// <summary>
    /// 阻塞续命锁
    /// </summary>
    /// <param name="key">阻塞锁的键</param>
    /// <param name="expireSeconds">阻塞锁的缓存时间</param>
    /// <param name="timeout">加锁超时时间</param>
    /// <param name="postponetime">续命时间</param>
    /// <returns></returns>
    public bool AcquireLock(string key, int expireSeconds, int timeout, int postponetime)
    {
        var script = @"local isNX = redis.call('SETNX', KEYS[1], ARGV[1])
                           if isNX == 1 then
                               redis.call('PEXPIRE', KEYS[1], ARGV[2])
                               return 1
                           end
                           return 0";
        RedisKey[] scriptkey = { key };
        RedisValue[] scriptvalues = { key, expireSeconds * 1000 };
        var stopwatch = Stopwatch.StartNew();
        while (stopwatch.Elapsed.TotalSeconds < timeout)
        {
            if (_database.ScriptEvaluate(script, scriptkey, scriptvalues).ToString() == "1")
            {
                stopwatch.Stop();
                //锁续命
                Thread postponeThread = new Thread(() =>
                {
                    postponeAction.Invoke(key, expireSeconds, postponetime, _database);
                });
                postponeThread.Start();
                return true;
            }
        }
        Console.WriteLine($"[{DateTime.Now}]{key}--阻塞锁超时");
        stopwatch.Stop();
        return false;
    }

    /// <summary>
    /// 阻塞锁--释放锁
    /// </summary>
    /// <param name="key"></param>
    /// <returns></returns>
    public bool UnAcquireLock(string key)
    {
        var script = @"local getLock = redis.call('GET', KEYS[1])
                            if getLock == ARGV[1] then
                              redis.call('DEL', KEYS[1])
                              return 1
                            end
                            return 0"
        ;
        RedisKey[] scriptkey = { key };
        RedisValue[] scriptvalues = { key };
        return _database.ScriptEvaluate(script, scriptkey, scriptvalues).ToString() == "1";
    }

    #endregion

    #endregion
}

.NET CORE中是没有现成的Redis锁续命的api,只能自己造轮子。续命同样使用了Redis的Lua脚本来实现,确保了原子性。获取了Redis锁之后,直接开启了一个新的线程,在设置时间还剩三分之一的时候进行了续命,这在程序中是有必要使用的,比如说因为网络原因造成的延时,本来我的这个接口执行完毕只需要3秒钟,但是有于网络延时造成了我的这个接口执行超过了3秒,这时候就需要Redis锁续命。以上代码就可以完美结局这个问题。

    [HttpGet("AcquireLockPostpone")]
    public void AcquireLockPostpone()
    {
        string key = Guid.NewGuid().ToString();
        if (_redisService.AcquireLock("AcquireLockPostpone", 3, 100, 3))
        {
            Thread.Sleep(5000);
            _redisService.UnAcquireLock("AcquireLockPostpone");
            Console.WriteLine($"AcquireLockPostpone--释放锁");
        }
    }

控制器API,调用可以续命的阻塞锁,缓存时间设置为3秒 续命时间也是延长3秒。我们走个100阻塞锁的并发试一下。

这100个阻塞锁均续命完成。也都正常执行完毕。


http://www.niftyadmin.cn/n/5457707.html

相关文章

STM32看似无法唤醒的一种异常现象分析

1. 引言 STM32 G0 系列产品具有丰富的外设和强大的处理性能以及良好的低功耗特性&#xff0c;被广泛用于各类工业产品中&#xff0c;包括一些需要低功耗需求的应用。 2. 问题描述 用户使用 STM32G0B1 作为汽车多媒体音响控制器的控制芯片&#xff0c;用来作为收音机频道存贮…

Ubuntu 安装Docker 和 Docker Compose

文章目录 一、docker安装1.1 卸载旧版本的Docker&#xff08;如果已安装&#xff09;1.2 更新Ubuntu的软件包列表&#xff1a;1.3 安装Docker所需的依赖包1.4 添加Docker的官方GPG密钥&#xff1a;1.5 再次更新软件包列表&#xff0c;并安装Docker引擎1.6 启动Docker服务并设置…

Floyd,最短路维护,LeetCode 2642. 设计可以求最短路径的图类

目录 一、题目 1、题目描述 2、接口描述 ​cpp python3 3、原题链接 二、解题报告 1、思路分析 2、复杂度 3、代码详解 ​cpp python3 一、题目 1、题目描述 给你一个有 n 个节点的 有向带权 图&#xff0c;节点编号为 0 到 n - 1 。图中的初始边用数组 edges 表示…

Hello算法6:哈希表

本文是基于k神的Hello 算法的读书笔记&#xff0c;请支持实体书。 https://www.hello-algo.com/chapter_paperbook/ 哈希表 通过建立key与value的映射&#xff0c;实现高效的查找。我们向哈希表输入一个值Key&#xff0c;就能在O(1)的时间内得到值Value。 哈希表的增删改查&…

NPM是什么?及常用的命令

npm是什么 NPM的全称是Node Package Manager&#xff0c;是随同NodeJS一起安装的包管理和分发工具&#xff0c;它很方便让JavaScript开发者下载、安装、上传以及管理已经安装的包。 npm常用命令 1、npm install express&#xff1a;安装Node模块 安装完毕后会产生一个node_…

unity学习(76)--窗口化和后台运行

1.通过如下方式将编译的游戏设置为窗口模式。 成功&#xff1a; 2.现在只有鼠标点击的窗体游戏运动&#xff0c;其他窗体游戏都会卡住。 2.1build setting中 2.2unity内部Project Settings 也被同步修改了

GPU算力池管理工具Determined AI部署与使用教程(2024.03)

1. 概念 1.1 什么是Determined&#xff1f; Determined AI 是一个全功能的深度学习平台&#xff0c;兼容 PyTorch 和 TensorFlow。它主要负责以下几个方面&#xff1a; 分布式训练&#xff1a;Determined AI 可以将训练工作负载分布在多个 GPU&#xff08;可能在多台计算机上…

Redis中的事件(一)

事件 概述 Redis服务器是一个事件驱动程序:服务器需要处理以下两类事件: 1.文件事件(file event):Redis服务器通过套接字与客户端(或者其他Redis服务器)进行连接&#xff0c;而文件事件就是服务器对套接字操作的抽象。服务器与客户端(或者其他服务器)的通信会产生相应的文件…