【问题标题】:Redis distributed increment with locking带锁的 Redis 分布式增量
【发布时间】:2021-07-12 15:53:00
【问题描述】:

我需要生成一个计数器,该计数器将发送到一些 api 调用。我的应用程序在多个节点上运行,所以我想如何生成唯一计数器。 我试过下面的代码

public static long GetTransactionCountForUser(int telcoId)
{
    long valreturn = 0;
    string key = "TelcoId:" + telcoId + ":Sequence";
    if (Muxer != null && Muxer.IsConnected && (Muxer.GetDatabase()) != null)
    {
        IDatabase db = Muxer.GetDatabase();
        var val = db.StringGet(key);
        int maxVal = 999;
        if (Convert.ToInt32(val) < maxVal)
        {
            valreturn = db.StringIncrement(key);
        }
        else
        {
            bool isdone = db.StringSet(key, valreturn);
            //db.SetAdd(key,new RedisValue) .StringIncrement(key, Convert.ToDouble(val))
        }
    }
    return valreturn;
}

并通过 Task Parallel 库对其进行运行测试。当我有边界值时,我看到的是设置了多次 0 条目

请让我知道我需要做什么更正

更新: 我的最终逻辑如下

public static long GetSequenceNumberForTelcoApiCallViaLuaScript(int telcoId)
{
    long valreturn = 0;
    long maxIncrement = 9999;//todo via configuration
    if (true)//todo via configuration
    {
        IDatabase db;
        string key = "TelcoId:" + telcoId + ":SequenceNumber";
        if (Muxer != null && Muxer.IsConnected && (db = Muxer.GetDatabase()) != null)
        {
            valreturn = (long)db.ScriptEvaluate(@"
                local result = redis.call('incr', KEYS[1])
                if result > tonumber(ARGV[1]) then
                result = 1
                redis.call('set', KEYS[1], result)
                end
                return result", new RedisKey[] { key }, flags: CommandFlags.HighPriority, values: new RedisValue[] { maxIncrement });
        }
    }
    return valreturn;
}

【问题讨论】:

  • 你为什么不使用一个只有一个标识列的简单表,做一个插入并使用返回的 SCOPE_IDENTITY() - 这应该总是返回一些唯一的东西。
  • 我想避免分贝插入/分贝往返。我支持通过 Redis 进行缓存,我想完全实现这一点
  • @KamranShahid 请不要使用string.Format 来参数化它;我将编辑我的示例以显示首选方式
  • 顺便说一句;我可能错了,但我认为GetDatabase 永远不会返回0,因此检查可能是多余的
  • 已使用参数值用法更新了我的答案。所以你认为没有Muxer连接但没有数据库的情况?

标签: c# redis .net-4.5 stackexchange.redis servicestack.redis


【解决方案1】:

确实,您的代码在翻转边界附近并不安全,因为您正在执行“获取”、(延迟和思考)、“设置” - 没有检查“获取”中的条件是否仍然适用。如果服务器忙于第 1000 项,则可能会得到各种疯狂的输出,包括:

1
2
...
999
1000 // when "get" returns 998, so you do an incr
1001 // ditto
1002 // ditto
0 // when "get" returns 999 or above, so you do a set
0 // ditto
0 // ditto
1

选项:

  1. 使用事务和约束 API 使您的逻辑并发安全
  2. 通过ScriptEvaluate将您的逻辑重写为Lua脚本

现在,redis 事务(根据选项 1)are hard。就个人而言,我会使用“2”——除了更简单的编码和调试之外,这意味着你只有 1 次往返和操作,而不是“get、watch、get、multi、incr/set、exec/丢弃”和“从开始重试”循环来说明中止情况。如果你愿意,我可以试着把它写成 Lua - 它应该是 4 行左右。


这是 Lua 的实现:

string key = ...
for(int i = 0; i < 2000; i++) // just a test loop for me; you'd only do it once etc
{
    int result = (int) db.ScriptEvaluate(@"
local result = redis.call('incr', KEYS[1])
if result > 999 then
    result = 0
    redis.call('set', KEYS[1], result)
end
return result", new RedisKey[] { key });
    Console.WriteLine(result);
}

注意:如果你需要参数化最大值,你会使用:

if result > tonumber(ARGV[1]) then

和:

int result = (int)db.ScriptEvaluate(...,
    new RedisKey[] { key }, new RedisValue[] { max });

(所以ARGV[1] 取值来自max

有必要了解eval/evalsha(这是ScriptEvaluate 调用的)不与其他服务器请求竞争,因此incr 和可能set。这意味着我们不需要复杂的watch 等逻辑。

通过事务/约束 API 也是一样的(我认为!):

static int IncrementAndLoopToZero(IDatabase db, RedisKey key, int max)
{
    int result;
    bool success;
    do
    {
        RedisValue current = db.StringGet(key);
        var tran = db.CreateTransaction();
        // assert hasn't changed - note this handles "not exists" correctly
        tran.AddCondition(Condition.StringEqual(key, current));
        if(((int)current) > max)
        {
            result = 0;
            tran.StringSetAsync(key, result, flags: CommandFlags.FireAndForget);
        }
        else
        {
            result = ((int)current) + 1;
            tran.StringIncrementAsync(key, flags: CommandFlags.FireAndForget);
        }
        success = tran.Execute(); // if assertion fails, returns false and aborts
    } while (!success); // and if it aborts, we need to redo
    return result;
}

很复杂,嗯?那么简单的成功案例是:

GET {key}    # get the current value
WATCH {key}  # assertion stating that {key} should be guarded
GET {key}    # used by the assertion to check the value
MULTI        # begin a block
INCR {key}   # increment {key}
EXEC         # execute the block *if WATCH is happy*

这是...相当多的工作,并且涉及多路复用器上的管道停顿。更复杂的情况(断言失败、监视失败、环绕)的输出会略有不同,但应该可以工作。

【讨论】:

  • 你能帮我在 stackexchange.redis 中的交易吗?顺便说一句,我在输出窗口中看到了相同的值:)
  • @KamranShahid 添加了一个可用的 Lua 示例;如果没有帮助,请告诉我
  • @KamranShahid 所示代码应该不受任何数量的连接的影响——这就是重点。通过重复,我假设您只是在谈论环绕时的意外情况。显然,您仍然会每 1000 个周期重复一次...即,如果您发出 1、2、3、... 999、0、1、2、3、4、... 998、999、0、1, 2 - 我们见过 1 三次。我假设您对此表示满意;p
  • 谢谢,非常好的和简单的实现,甚至更好的解释。
  • @KamranShahid 是的; Lua 和 multi/exec 在集群上都很好 只要 a: 它们只影响单个哈希槽(如果它们只有一个键,则必须是这种情况)和 b:(在特别是 Lua 的情况)您使用 KEYS 传递密钥,而不是 ARGV(它使用 KEYS 进行哈希槽路由)
【解决方案2】:

您可以使用WATCH command - 这样,如果值发生变化,您会收到通知

【讨论】:

  • 知道如何在 stackexchange.redis api 中获得优势吗?
  • 注意:这不是我的建议; redis 事务 API 很难正确处理 - 在这种情况下 Lua 会简单得多 (/cc @KamranShahid)
  • @KamranShahid 两者都不是;这很难 - 给我几分钟,我将尝试通过我的答案中的事务/约束 API 编写等效代码
  • @KamranShahid 添加到我的答案中
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-11-05
  • 2021-06-01
  • 1970-01-01
  • 2022-05-13
  • 2014-01-11
  • 1970-01-01
  • 2021-11-18
相关资源
最近更新 更多