云计算百科
云计算领域专业知识百科平台

C#,VB.NET多台电脑读取REDIS服务器,如何保证数据不会冲突

多台电脑读取REDIS服务器,如何保证数据不会冲突?比如这台扣了10元,另一台打开也要扣钱,用原子算法肯定不会扣错吧?

已完成思考

你问到了 Redis 多实例并发的核心场景!先给100% 确定的结论:

  • 如果是「直接扣钱」(单命令操作,如 DecrBy balance 10),用 Redis 原子操作绝对不会扣错;
  • 如果是「先读余额→判断够不够→再扣钱」(多步操作),需要用 Lua 脚本 把多步封装成原子操作,也能保证不扣错。

核心原因是 Redis 的「单线程模型」+「原子命令 / 脚本」机制,我帮你把这个逻辑讲透,再给你可直接落地的 VB.NET 代码。


一、先明确:Redis 为什么能保证多实例不冲突?

Redis 服务端是单线程处理命令的 —— 不管有多少台电脑(多实例)同时连 Redis,服务端都会「按顺序一个一个执行命令」,同一时间只有一个命令在执行,不会有并发打断的问题。

这是 Redis 原子性的基础:单命令天然原子,多步操作只要封装成单命令 / 脚本也能原子。


二、场景 1:直接扣钱(单命令,绝对不会错)

如果你的逻辑是「不管余额多少,直接扣 10 元」(或余额足够的前提下直接扣),用 Redis 原子命令 DecrBy(或 IncrBy -10)即可,多实例同时调用也不会扣错。

核心逻辑:
  • 单命令 DecrBy balance 10 是原子的,Redis 服务端会一次性完成「读取当前值→减 10→写入新值」,中间不会被其他实例打断;
  • 不管有 10 台还是 100 台电脑同时扣钱,最终结果都是「初始余额 – 总扣钱数」,不会少扣或多扣。
VB.NET 代码(适配你的 CSRedisClient):

vb

Imports CSRedis

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

''' <summary>
''' 直接扣钱(原子操作,多实例绝对安全)
''' </summary>
''' <param name="userId">用户ID</param>
''' <param name="amount">扣钱金额(正数)</param>
''' <returns>扣后的余额</returns>
Public Function DeductMoneyDirect(userId As String, amount As Integer) As Long
Dim key = $"account:balance:{userId}"
' 核心:原子减 amount(等价于 DecrBy,适配低版本用 IncrBy -amount)
Dim newBalance = _redisClient.IncrBy(key, -amount)
Return newBalance
End Function
End Class

' ========== 多实例并发测试(模拟10台电脑同时扣钱) ==========
Sub Main()
Dim service As New AccountService()
' 初始化余额为 1000
_redisClient.Set("account:balance:1001", "1000")

' 模拟10台电脑同时扣10元
Dim tasks As New List(Of Task)
For i As Integer = 1 To 10
tasks.Add(Task.Run(Sub()
Dim balance = service.DeductMoneyDirect("1001", 10)
Debug.WriteLine($"实例{i}扣钱后余额:{balance}")
End Sub))
Next
Task.WaitAll(tasks.ToArray())

' 最终余额一定是 1000 – 10*10 = 900,绝对不会错
Dim finalBalance = _redisClient.Get("account:balance:1001")
Debug.WriteLine($"最终余额:{finalBalance}") ' 输出:900
End Sub


三、场景 2:先读余额→判断够不够→再扣钱(多步操作,需 Lua 脚本)

如果你的逻辑是「先读余额,判断≥10 元才扣钱,否则提示余额不足」,这是多步操作,单命令原子性不够(因为「读」和「扣」之间可能被其他实例打断),需要用 Lua 脚本 把多步封装成原子操作。

为什么单命令不够?(举反例)

假设初始余额是 15 元,两台电脑同时扣 10 元:

  • 实例 A 读余额:15 元(够扣);
  • 实例 B 读余额:15 元(够扣);
  • 实例 A 扣 10 元:余额变成 5 元;
  • 实例 B 扣 10 元:余额变成 – 5 元(超扣了,逻辑错误)。
  • 解决方案:Lua 脚本(原子执行多步)

    Redis 执行 Lua 脚本是原子的—— 脚本执行期间不会被其他命令打断,相当于把「读→判断→扣」变成了一个单命令。

    VB.NET 完整代码(适配 CSRedisClient):

    vb

    Imports CSRedis
    Imports System.Collections.Generic

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

    ''' <summary>
    ''' 先判断余额再扣钱(Lua 脚本原子操作,多实例绝对安全)
    ''' </summary>
    ''' <param name="userId">用户ID</param>
    ''' <param name="amount">扣钱金额(正数)</param>
    ''' <returns>扣后的余额(负数表示余额不足)</returns>
    Public Function DeductMoneyWithCheck(userId As String, amount As Integer) As Long
    Dim key = $"account:balance:{userId}"

    ' Lua 脚本:原子执行「读余额→判断→扣钱」
    ' 逻辑:1. 读余额;2. 若余额<amount,返回-1;3. 否则扣钱并返回新余额
    Dim luaScript = "
    local balance = tonumber(redis.call('GET', KEYS[1]))
    if balance == nil then
    return -2 — 账户不存在
    end
    if balance < tonumber(ARGV[1]) then
    return -1 — 余额不足
    end
    return redis.call('INCRBY', KEYS[1], -tonumber(ARGV[1])) — 扣钱
    "

    ' 执行 Lua 脚本(原子操作)
    ' KEYS[1] = 余额Key,ARGV[1] = 扣钱金额
    Dim result = _redisClient.Eval(luaScript, New List(Of String) From {key}, amount.ToString())
    Return CLng(result)
    End Function
    End Class

    ' ========== 多实例并发测试(模拟10台电脑同时扣钱,余额15元) ==========
    Sub Main()
    Dim service As New AccountService()
    ' 初始化余额为 15 元
    _redisClient.Set("account:balance:1001", "15")

    ' 模拟10台电脑同时扣10元
    Dim tasks As New List(Of Task)
    For i As Integer = 1 To 10
    tasks.Add(Task.Run(Sub()
    Dim result = service.DeductMoneyWithCheck("1001", 10)
    If result >= 0 Then
    Debug.WriteLine($"实例{i}扣钱成功,余额:{result}")
    ElseIf result = -1 Then
    Debug.WriteLine($"实例{i}扣钱失败:余额不足")
    ElseIf result = -2 Then
    Debug.WriteLine($"实例{i}扣钱失败:账户不存在")
    End If
    End Sub))
    Next
    Task.WaitAll(tasks.ToArray())

    ' 最终结果:只有1台电脑能扣成功,余额变成5元,其他9台提示余额不足,绝对不会超扣
    Dim finalBalance = _redisClient.Get("account:balance:1001")
    Debug.WriteLine($"最终余额:{finalBalance}") ' 输出:5
    End Sub


    四、关键避坑点(绝对不能犯)

  • 不要用客户端锁(如 VB.NET 的 SyncLock):SyncLock 只能锁住「当前电脑的当前进程」,多台电脑之间完全没用,必须用 Redis 服务端的原子操作 / Lua 脚本 / 分布式锁。

  • 不要把「读」和「扣」分成两个命令:比如先 Get balance 再 DecrBy balance 10,中间会被其他实例打断,导致超扣,必须用 Lua 脚本封装成原子操作。

  • Lua 脚本不要写太复杂的逻辑:Redis 执行 Lua 脚本时会阻塞其他命令,所以脚本要尽量短平快,只放「读→判断→扣」的核心逻辑。


  • 五、总结

  • 直接扣钱:用 DecrBy(或 IncrBy -amount)单命令,绝对不会扣错;
  • 先判断再扣钱:用 Lua 脚本把「读→判断→扣」封装成原子操作,绝对不会超扣;
  • 核心原理:Redis 单线程模型 + 原子命令 / 脚本,保证多实例并发时同一时间只有一个操作在执行;
  • 避坑点:不要用客户端锁,不要把多步操作分开。
  • 这套方案是 Redis 多实例并发扣钱的「工业级标准写法」,不管有多少台电脑同时操作,都能保证数据 100% 正确,你可以直接落地使用。

    「读」和「扣」要用LUA脚本去操作就变复杂了。如果增加像是SQL句子一样的, MONEY=MOEY-10 WHERE MONEY>10 同时返回MOMEY余额,一句话实现,或者有个SQL句子中的锁,多个SQL句子执行完了再解锁 比如decrbyCheck(key,-amount,money-amount>10) 代表扣款后余额要大于10的才能操作,最少也要满足 money>=amount 其实redis中增加一个这样的接口不难呀,为什么不做?

    已完成思考

    发现用户对 Redis 不提供类似 SQL 的条件原子操作接口不满,觉得 Lua 脚本复杂。我判断用户核心诉求是希望有更简单的、类似 SQL 的条件原子操作,无需编写 Lua 脚本。

    已完成

    你的想法非常直观!其实 Redis 不是「做不到」,而是设计哲学上选择了「提供通用机制(Lua 脚本 / Redis Functions),而非无限增加特定场景命令」—— 而且你想要的「条件扣钱 + 返回余额」,用 Lua 脚本封装成通用方法后,调用起来和原生命令一样简单,我帮你把这个逻辑讲透,再给你一个「一行代码调用」的封装方案。


    一、先回答:为什么 Redis 不内置 DecrByCheck 这样的特定命令?

    Redis 的核心设计哲学是「保持简单、通用,避免命令集爆炸」:

  • 场景太多,无法穷举:你需要的是「扣钱后余额≥10」,但还有人需要「扣钱后余额≥0」「扣钱后余额在 10-100 之间」「扣钱同时增加积分」…… 如果每个场景都加一个命令,Redis 的命令集会变得无比臃肿,维护成本极高;
  • 通用机制已足够:Redis 提供了 Lua 脚本(Redis 2.6+)和 Redis Functions(Redis 7.0+),让你可以自定义任意条件逻辑,本质上就是你想要的「SQL 式条件操作」,而且更灵活;
  • 性能无差异:Lua 脚本在 Redis 服务端是原子执行的,和原生命令的性能几乎一样,没有额外开销。

  • 二、核心方案:把 Lua 脚本封装成「通用方法」,调用起来和原生命令一样简单

    你不需要每次都写 Lua 脚本,只需要封装一次,后续调用就像 DecrByCheck(key, amount, minBalance) 一样简单,我帮你写好可直接复用的 VB.NET 代码:

    vb

    Imports CSRedis
    Imports System.Collections.Generic

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

    ''' <summary>
    ''' 通用条件扣钱方法(封装 Lua 脚本,调用起来和原生命令一样简单)
    ''' 等价于:MONEY = MONEY – amount WHERE MONEY >= minBalance,同时返回扣后余额
    ''' </summary>
    ''' <param name="key">余额 Key</param>
    ''' <param name="amount">扣钱金额(正数)</param>
    ''' <param name="minBalance">扣钱前/后需满足的最低余额(比如 amount 表示扣前够扣,10 表示扣后≥10)</param>
    ''' <param name="checkBeforeDeduct">true=检查扣前余额≥minBalance;false=检查扣后余额≥minBalance</param>
    ''' <returns>≥0:扣后余额;-1:余额不足;-2:Key不存在</returns>
    Public Function DecrByCheck(key As String, amount As Integer, minBalance As Integer, Optional checkBeforeDeduct As Boolean = True) As Long
    ' 只需要写一次 Lua 脚本,后续永远复用
    Dim luaScript = "
    local current = tonumber(redis.call('GET', KEYS[1]))
    if current == nil then
    return -2 — Key 不存在
    end
    local amount = tonumber(ARGV[1])
    local min = tonumber(ARGV[2])
    local checkBefore = tonumber(ARGV[3]) == 1

    if checkBefore then
    — 检查扣前余额:current >= min(min 一般等于 amount)
    if current < min then
    return -1
    end
    else
    — 检查扣后余额:current – amount >= min
    if current – amount < min then
    return -1
    end
    end

    — 执行扣钱并返回新余额
    return redis.call('INCRBY', KEYS[1], -amount)
    "

    ' 执行脚本(参数:KEYS[1]=key,ARGV[1]=amount,ARGV[2]=minBalance,ARGV[3]=checkBeforeDeduct)
    Dim result = _redisClient.Eval(
    luaScript,
    New List(Of String) From {key},
    amount.ToString(),
    minBalance.ToString(),
    If(checkBeforeDeduct, "1", "0")
    )
    Return CLng(result)
    End Function
    End Class

    ' ========== 调用示例(一行代码,和原生命令一样简单) ==========
    Sub Main()
    Dim helper As New RedisAtomicHelper()
    Dim key = "account:balance:1001"

    ' 初始化余额为 100
    _redisClient.Set(key, "100")

    ' 场景1:扣10元,要求扣前余额≥10(最常用,等价于 MONEY = MONEY-10 WHERE MONEY>=10)
    Dim result1 = helper.DecrByCheck(key, 10, 10, True)
    If result1 >= 0 Then
    Debug.WriteLine($"场景1扣钱成功,余额:{result1}") ' 输出:90
    End If

    ' 场景2:扣10元,要求扣后余额≥85(等价于 MONEY = MONEY-10 WHERE MONEY-10>=85)
    Dim result2 = helper.DecrByCheck(key, 10, 85, False)
    If result2 >= 0 Then
    Debug.WriteLine($"场景2扣钱成功,余额:{result2}") ' 输出:80
    End If

    ' 场景3:扣100元,余额不足(返回-1)
    Dim result3 = helper.DecrByCheck(key, 100, 100, True)
    If result3 = -1 Then
    Debug.WriteLine($"场景3扣钱失败:余额不足")
    End If
    End Sub


    三、进阶:Redis 7.0+ 的「Redis Functions」—— 完全像原生命令一样调用

    如果你用的是 Redis 7.0+,可以用 Redis Functions 把自定义逻辑预加载到 Redis 服务端,后续调用就和原生命令一模一样,连 Lua 脚本都不用在客户端写了:

    1. 预加载 Function 到 Redis(只需执行一次)

    bash

    运行

    # 在 redis-cli 中执行,预加载一个名为 deduct_check 的函数
    redis-cli -a cd@JD34
    > FUNCTION LOAD "#!lua name=deduct_check
    local function deduct_check(keys, args)
    local current = tonumber(redis.call('GET', keys[1]))
    if current == nil then return -2 end
    local amount = tonumber(args[1])
    local min = tonumber(args[2])
    if current < min then return -1 end
    return redis.call('INCRBY', keys[1], -amount)
    end
    redis.register_function('deduct_check', deduct_check)"

    2. VB.NET 调用(和原生命令完全一样)

    vb

    ' 调用预加载的 Function,就像调用原生 DecrBy 一样简单
    Dim result = _redisClient.Call("FCALL", "deduct_check", "1", key, amount.ToString(), minBalance.ToString())


    四、总结

  • 为什么 Redis 不内置特定命令:设计哲学是「提供通用机制,而非穷举场景」,避免命令集爆炸;
  • 你的需求完全可以满足:把 Lua 脚本封装成通用方法后,调用起来和原生命令一样简单(一行代码 DecrByCheck(key, amount, minBalance));
  • 进阶方案:Redis 7.0+ 可用 Redis Functions 预加载逻辑,调用体验和原生命令完全一致;
  • 核心优势:封装后的方法既保持了原子性(多实例不冲突),又足够灵活(支持任意条件),性能和原生命令无差异。
  • 其实你想要的「SQL 式条件操作」,Redis 已经通过 Lua 脚本 / Redis Functions 提供了,只是需要你封装一次,后续就可以像用原生命令一样简单,而且更灵活 —— 这个方案是 Redis 生态中处理「自定义条件原子操作」的标准做法,你可以直接落地使用。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » C#,VB.NET多台电脑读取REDIS服务器,如何保证数据不会冲突
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!