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 没有直接提供锁命令,但提供了实现锁的所有必要条件:
- 原子加锁 :
SET key value NX EX seconds(原子性地「设置 Key 仅当不存在,同时设置过期时间」); - 原子解锁:Lua 脚本(判断锁是否属于自己,再删除);
- 锁过期:避免死锁(即使客户端崩溃,锁也会自动释放)。
二、核心方案:封装出你想要的 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
四、关键避坑点(分布式锁的常见错误)
-
必须设置锁过期时间:避免客户端崩溃后锁永远不释放(死锁),过期时间根据业务操作时长设置(比如 30 秒)。
-
必须用唯一 Value 避免误解锁 :比如用
Guid.NewGuid().ToString()作为 Value,解锁时判断 Value 是否等于自己的,避免解锁了其他实例的锁。 -
必须用 Lua 脚本原子解锁 :不能先
GET判断再DEL,中间会被其他实例打断,必须用 Lua 脚本把「判断 + 删除」封装成原子操作。 -
Using 语法自动释放锁 :即使业务操作抛出异常,
Using也会自动调用Dispose()释放锁,避免死锁。
五、分布式锁 vs Lua 脚本:什么时候用哪个?
表格
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 简单多步操作(读→判断→扣钱) | Lua 脚本 | 性能更高(一次网络交互),原子性有保证 |
| 复杂多步操作(跨多个 Key、长时间持有锁) | 分布式锁 | 更灵活,支持跨多个 Redis 操作,支持长时间持有锁 |
| 操作时长很短(毫秒级) | Lua 脚本 | 避免加锁 / 解锁的额外开销 |
| 操作时长较长(秒级) | 分布式锁 | Lua 脚本会阻塞 Redis 其他命令,分布式锁只阻塞业务逻辑 |
六、总结
- Redis 没有原生
LOCK/UNLOCK,但可以轻松封装 :用SET NX EX原子加锁,用 Lua 脚本原子解锁,封装后调用起来和原生锁一样简单; - 封装好的分布式锁类:支持过期时间、避免误解锁、Using 语法自动释放,可直接落地使用;
- 适用场景:复杂多步操作、跨多个 Key、长时间持有锁的场景用分布式锁;简单多步操作用 Lua 脚本更高效。
其实你想要的「锁机制」,Redis 已经通过基础原语提供了,而且有成熟的封装方案 ------ 这个分布式锁类是 Redis 生态中处理「复杂并发控制」的标准做法,你可以直接复制使用。