c#Redis扣款锁的设计,多用户,多台电脑操作

Redis 虽然没有原生的 LOCK(key)/UNLOCK(key) 命令 ,但提供了实现分布式锁的核心原语SET NX EX),可以轻松封装出你想要的 Lock/Unlock 接口 ------ 而且这是 Redis 生态中处理「复杂多步操作并发控制」的标准方案。

我帮你把这个逻辑讲透,再给你一个可直接复用的 VB.NET 分布式锁封装,同时对比「分布式锁 vs Lua 脚本」的适用场景。

C#,VB.NET多台电脑读取REDIS服务器,如何保证数据不会冲突-CSDN博客
https://blog.csdn.net/xiaoyao961/article/details/158208482


一、先明确:Redis 为什么不直接提供 LOCK/UNLOCK

还是那个核心设计哲学:保持核心简单,提供基础原语,让用户自己构建高级功能

Redis 没有直接提供锁命令,但提供了实现锁的所有必要条件

  1. 原子加锁SET key value NX EX seconds(原子性地「设置 Key 仅当不存在,同时设置过期时间」);
  2. 原子解锁:Lua 脚本(判断锁是否属于自己,再删除);
  3. 锁过期:避免死锁(即使客户端崩溃,锁也会自动释放)。

二、核心方案:封装出你想要的 Lock/Unlock 接口

我帮你写好一个线程安全、支持过期、避免误解锁VB.NET 分布式锁封装,调用起来和你想的一模一样:

vb

vbnet 复制代码
Imports CSRedis
Imports System
Imports System.Collections.Generic
Imports System.Threading

Public Class RedisDistributedLock
    Private Shared _redisClient As New CSRedisClient("127.0.0.1:6379,password=cd@JD34,defaultDatabase=0")
    Private _lockKey As String
    Private _lockValue As String
    Private _expireSeconds As Integer
    Private _isLocked As Boolean = False

    ''' <summary>
    ''' 初始化分布式锁
    ''' </summary>
    ''' <param name="key">锁的 Key(比如 "lock:account:1001")</param>
    ''' <param name="expireSeconds">锁的过期时间(秒,避免死锁)</param>
    Public Sub New(key As String, Optional expireSeconds As Integer = 30)
        _lockKey = key
        _expireSeconds = expireSeconds
        _lockValue = Guid.NewGuid().ToString() ' 唯一标识,避免误解锁别人的锁
    End Sub

    ''' <summary>
    ''' 尝试加锁(对应你想要的 RedisA.LOCK(key))
    ''' </summary>
    ''' <param name="waitTimeoutMs">等待获取锁的超时时间(毫秒,0 表示不等待)</param>
    ''' <returns>是否加锁成功</returns>
    Public Function TryLock(Optional waitTimeoutMs As Integer = 0) As Boolean
        Dim startTime = DateTime.Now
        While True
            ' 核心:原子加锁(SET NX EX)
            ' NX:仅当 Key 不存在时设置;EX:设置过期时间
            Dim lockSuccess = _redisClient.Set(_lockKey, _lockValue, TimeSpan.FromSeconds(_expireSeconds), RedisExistence.Nx)
            If lockSuccess Then
                _isLocked = True
                Return True
            End If

            ' 如果超时,返回失败
            If (DateTime.Now - startTime).TotalMilliseconds >= waitTimeoutMs Then
                Return False
            End If

            ' 等待一小段时间再重试(避免 CPU 空转)
            Thread.Sleep(50)
        End While
    End Function

    ''' <summary>
    ''' 释放锁(对应你想要的 RedisA.UNLOCK(key))
    ''' </summary>
    Public Sub Unlock()
        If Not _isLocked Then Return

        ' 核心:原子解锁(Lua 脚本,判断锁是否属于自己,再删除)
        ' 避免误解锁:只有锁的 Value 等于自己的 _lockValue 时才删除
        Dim luaScript = "
            if redis.call('GET', KEYS[1]) == ARGV[1] then
                return redis.call('DEL', KEYS[1])
            else
                return 0
            end
        "
        _redisClient.Eval(luaScript, New List(Of String) From {_lockKey}, _lockValue)
        _isLocked = False
    End Sub

    ''' <summary>
    ''' 自动释放锁(Using 语法支持)
    ''' </summary>
    Public Sub Dispose() Implements IDisposable.Dispose
        Unlock()
    End Sub
End Class

三、调用示例(和你想的一模一样,简单直观)

用这个封装好的类,你可以像用原生 LOCK/UNLOCK 一样处理多步操作,多台电脑同时操作也不会冲突:

vb

复制代码
Imports CSRedis

Public Class AccountService
    Private Shared _redisClient As New CSRedisClient("127.0.0.1:6379,password=cd@JD34,defaultDatabase=0")

    ''' <summary>
    ''' 用分布式锁实现「先读余额→判断够不够→再扣钱→再增加积分」(复杂多步操作)
    ''' </summary>
    Public Sub DeductMoneyWithLock(userId As String, amount As Integer)
        Dim balanceKey = $"account:balance:{userId}"
        Dim lockKey = $"lock:account:{userId}" ' 锁的 Key,和业务 Key 对应

        ' 1. 加锁(Using 语法,自动释放锁,避免死锁)
        Using lock As New RedisDistributedLock(lockKey, 30)
            If Not lock.TryLock(1000) Then ' 等待 1000ms 获取锁
                Debug.WriteLine("获取锁失败,其他实例正在操作")
                Return
            End If

            ' 2. 加锁成功后,执行多步操作(中间不会被其他实例打断)
            Dim balance = CLng(_redisClient.Get(balanceKey))
            If balance < amount Then
                Debug.WriteLine("余额不足")
                Return
            End If

            ' 扣钱
            _redisClient.IncrBy(balanceKey, -amount)
            ' 增加积分(假设这是另一个 Redis 操作)
            _redisClient.IncrBy($"account:points:{userId}", amount * 10)

            Debug.WriteLine($"扣钱成功,余额:{balance - amount},积分增加:{amount * 10}")
        End Using ' 3. 自动释放锁(即使中间抛出异常,也会释放)
    End Sub
End Class

' ========== 多实例并发测试(模拟10台电脑同时操作) ==========
Sub Main()
    Dim service As New AccountService()
    Dim userId = "1001"
    ' 初始化余额 100,积分 0
    _redisClient.Set($"account:balance:{userId}", "100")
    _redisClient.Set($"account:points:{userId}", "0")

    ' 模拟10台电脑同时扣10元
    Dim tasks As New List(Of Task)
    For i As Integer = 1 To 10
        tasks.Add(Task.Run(Sub()
                               service.DeductMoneyWithLock(userId, 10)
                           End Sub))
    Next
    Task.WaitAll(tasks.ToArray())

    ' 最终结果:只有10次扣钱都成功(余额0,积分1000),或者部分成功(取决于初始余额),但不会超扣,也不会少加积分
    Dim finalBalance = _redisClient.Get($"account:balance:{userId}")
    Dim finalPoints = _redisClient.Get($"account:points:{userId}")
    Debug.WriteLine($"最终余额:{finalBalance},最终积分:{finalPoints}")
End Sub

四、关键避坑点(分布式锁的常见错误)

  1. 必须设置锁过期时间:避免客户端崩溃后锁永远不释放(死锁),过期时间根据业务操作时长设置(比如 30 秒)。

  2. 必须用唯一 Value 避免误解锁 :比如用 Guid.NewGuid().ToString() 作为 Value,解锁时判断 Value 是否等于自己的,避免解锁了其他实例的锁。

  3. 必须用 Lua 脚本原子解锁 :不能先 GET 判断再 DEL,中间会被其他实例打断,必须用 Lua 脚本把「判断 + 删除」封装成原子操作。

  4. Using 语法自动释放锁 :即使业务操作抛出异常,Using 也会自动调用 Dispose() 释放锁,避免死锁。


五、分布式锁 vs Lua 脚本:什么时候用哪个?

表格

场景 推荐方案 原因
简单多步操作(读→判断→扣钱) Lua 脚本 性能更高(一次网络交互),原子性有保证
复杂多步操作(跨多个 Key、长时间持有锁) 分布式锁 更灵活,支持跨多个 Redis 操作,支持长时间持有锁
操作时长很短(毫秒级) Lua 脚本 避免加锁 / 解锁的额外开销
操作时长较长(秒级) 分布式锁 Lua 脚本会阻塞 Redis 其他命令,分布式锁只阻塞业务逻辑

六、总结

  1. Redis 没有原生 LOCK/UNLOCK,但可以轻松封装 :用 SET NX EX 原子加锁,用 Lua 脚本原子解锁,封装后调用起来和原生锁一样简单;
  2. 封装好的分布式锁类:支持过期时间、避免误解锁、Using 语法自动释放,可直接落地使用;
  3. 适用场景:复杂多步操作、跨多个 Key、长时间持有锁的场景用分布式锁;简单多步操作用 Lua 脚本更高效。

其实你想要的「锁机制」,Redis 已经通过基础原语提供了,而且有成熟的封装方案 ------ 这个分布式锁类是 Redis 生态中处理「复杂并发控制」的标准做法,你可以直接复制使用。

相关推荐
闲人编程1 天前
定时任务与周期性调度
分布式·python·wpf·调度·cron·定时人物·周期性
zls3653651 天前
C# WPF canvas中绘制缺陷分布map并实现缩放
开发语言·c#·wpf
数据知道2 天前
PostgreSQL:Citus 分布式拓展,水平分片,支持海量数据与高并发
分布式·postgresql·wpf
闲人编程3 天前
Redis分布式锁实现
redis·分布式·wpf·进程··死锁·readlock
之歆3 天前
ZooKeeper 分布式协调服务完全指南
分布式·zookeeper·wpf
sdff113964 天前
【HarmonyOS】鸿蒙Flutter跨设备流转技术实战指南
flutter·wpf·harmonyos
wzqllwy5 天前
java实战-分布式事务
wpf
YrqnxehxDo6 天前
相场模拟——合金,金属凝固模型,各向异性枝晶生长karma 合金凝固模型,选区激光熔融,激光增...
wpf