一个客户端通过Redis WATCH MULTI EXEC的方式进行操作。

11

我正在使用RedisOnGo + node_redis作为客户端,结合NodeJS + Express + Redis。由于我希望实现高并发,因此尝试测试WATCH。该示例不包含Express,只有必要的内容。

var redis = require("redis")
var rc = redis.createClient(config.redis.port, config.redis.host)

rc.auth(config.redis.hash, function(err) {
    if (err) {
        throw err
    }
})

rc.on('ready', function () {
    rc.set("inc",0)
    for(var i=1;i<=10;i++){
        rc.watch("inc")
        rc.get("inc",function(err,data){
            var multi = rc.multi()
            data++ // I do know I can use rc.incr(), this is just for example
            multi.set("inc",data)
            multi.exec(function(err,replies){
                console.log(replies)
            })
        })
    }
})
期望结果: 在执行回调函数时得到N个错误,最终得到"inc"变量=10-N。 意外的结果: 在执行回调函数时得到0个错误,但最终得到"inc"变量=1。
我的代码中Watch不起作用。
我找到了这个主题redis and watch + multi allows concurrent users。他们说这是因为只有一个redis客户端。
然后我找到了这个主题Should I create a new Redis client for each connection?。他们说每个事务生成一个新客户端“绝对不推荐”。我很迷茫。
请注意,我必须对Redis服务器进行身份验证。谢谢! 更新 1: 我能够使用本地Redis实例(因此我不使用client.auth)在每个WATCH-MULTI-EXEC迭代之前创建一个新的客户端连接来使其工作。不确定它是否好,但现在的结果是100%准确的。 更新 2:如果我在每个WATCH-MULTI-EXEC迭代之前创建一个新的客户端连接,然后做client.auth并等待client.on,我就能使它工作。
问题仍然存在,我为每个迭代创建新的客户端连接是否可以?
4个回答

23

你的结果是完全可预测的。而且理所当然。

请记住 - node.js 是一个单线程应用程序。Node.js 使用异步输入输出,但命令必须以严格的“请求-响应”顺序发送到 redis 中。因此,在使用一个连接到 redis 服务器时,您的代码和请求会严格并行执行。

看看你的代码:

rc.on('ready', function () {
    rc.set("inc",0)
    for(var i = 1; i <= 10; i++){
        rc.watch("inc")
        //10 times row by row call get function. It`s realy means that your written
        //in an asynchronous style code executed strict in series. You are using just
        //one connection - so all command would be executed one by one.
        rc.get("inc",function(err,data){
            //Your data variable data = 0 for each if request.
            var multi = rc.multi()
            data++ //This operation is not atomic for redis so your always has data = 1
            multi.set("inc",data) //and set it
            multi.exec(function(err,replies){
                console.log(replies) 
            })
        })
    }
})

要确认这一点,请执行以下步骤:

  1. 连接到 Redis 并执行 monitor 命令。
  2. 运行你的 node.js 应用程序

输出将会是:

    SET inc 0
    WATCH inc

    GET inc 
    .... get command more 9 times

    MULTI
    SET inc 1
    EXEC
    .... command block more 9 times

为了确保您获得与上述内容完全相同的结果:“在执行回调时获得0个错误,但最终获得“inc”变量=1。”。

您为每次迭代创建新的客户端连接,这样可以吗?

对于此示例-是的,它可以解决您的问题。一般来说-这取决于您要运行多少个“并发”查询。Redis仍然是单线程的,因此这里的“并发”只是将命令批处理发送到Redis引擎的方式。

例如,如果使用2个连接,则monitor可能会产生类似于以下内容:

 1 SET inc 0 //from 1st connection
 2 WATCH inc //from 1st connection
 3 SET inc 0 //from 2nd connection            
 4 GET inc //from 1nd connection            
 5 WATCH int //from 2nd connection       
 6 GET inc //from 2nd connection                 
 7 MULTI //from 1st connection           
 8 SET inc 1 //from 1st connection    
 9 MULTI //from 2nd connection           
10 SET inc 1 //from 2nd connection           
11 EXEC //from 1st failed becouse of 2nd connection SET inc 0 (line 3) 
        //was executed after WATCH (line 2) 
12 EXEC //success becouse of MULTI from 1st connection was failed and SET inc 1 from first 
        //connection was not executed

-------------------------------------------------------------------------------> time 
               |   |    |  |   |     |     |    |   |     |    |         |
connection 1  set watch | get  |     |   multi set  |     |   exec(fail) |
connection 2          set    watch  get            multi set            exec

了解Redis如何执行您的命令非常重要。Redis是单线程的,所有连接的所有命令都将依次执行。Redis不能保证来自一个连接的命令会依次执行(如果有其他连接存在),因此如果需要确保您的命令以一块执行,则应使用MULTI命令。但是为什么需要WATCH?查看上面的我的Redis命令。您可以看到来自不同连接的命令被混合在一起。而watch使您能够管理它们。

这在文档中得到了很好的解释。请阅读该文档!


谢谢,你的回答似乎相当详细,并且你正确理解了我的问题。如果在两天内没有更多的答案,赏金就归你了。 - igorpavlov

2

我终于明白了你的问题。

如果你想测试WATCH并发性,我认为你需要改变你的代码。我们知道,WATCH只监视值的变化,而不是获取值的操作。所以在你当前的代码中,所有的get命令都将成功执行并获取到0,然后它们将把inc设置为1。所有设置的值都相同(1),因此监视不会失败。

在这种情况下,我们需要确保不仅保护write操作,还要保护read操作。在你设置inc之前,你需要watch并修改另一个键作为悲观锁,然后我们可以获取和更改inc。这样,它将确保你的期望。

rc.set("inc",0)
for(var i=1;i<=10;i++){
    rc.watch("inc-lock")
    rc.get("inc",function(err,data){
        var multi = rc.multi()
        data++
        multi.incr("inc-lock")
        multi.set("inc",data)
        multi.exec(function(err,replies){
            console.log(replies)
        })
    })
}

我在我的电脑上进行了测试。

[2013-11-26 18:51:09.389] [INFO] console - [ 1, 'OK' ]

[2013-11-26 18:51:09.390] [INFO] console - [ 2, 'OK' ]

[2013-11-26 18:51:09.390] [INFO] console - [ 3, 'OK' ]

[2013-11-26 18:51:09.390] [INFO] console - [ 4, 'OK' ]

[2013-11-26 18:51:09.391] [INFO] console - [ 5, 'OK' ]

[2013-11-26 18:51:09.391] [INFO] console - [ 6, 'OK' ]

[2013-11-26 18:51:09.392] [INFO] console - [ 7, 'OK' ]

[2013-11-26 18:51:09.392] [INFO] console - [ 8, 'OK' ]

[2013-11-26 18:51:09.393] [INFO] console - [ 9, 'OK' ]

[2013-11-26 18:51:09.393] [INFO] console - [ 10, 'OK' ]


1
谢谢您的回答。然而,它并没有解决我的问题。我理解Redis的排队过程,但这里的问题非常简单:我有一个单节点进程,如果我在应用程序启动时连接到Redis一次,并尝试使用此连接来处理所有查询 - WATCH似乎不起作用。如果每次执行“事务”时都创建新的连接,则WATCH可以正常工作并实际监视键。但我不想每次需要向Redis发出请求时都连接到它。这就是问题所在。 - igorpavlov
再次感谢您的时间,但仍然不正确。在代码中,我写了一个注释“//我知道我可以使用rc.incr(),这只是为了举例”。我知道incr()是原子操作。但是,在此代码内部,可能会有许多不同的非原子操作,例如GET->SET。此外,您不需要使用WATCH与incr()一起使用。 - igorpavlov
我猜你不明白我的代码意思。我增加了一个inc-lock key来添加一个watch锁。我们的真正inc逻辑并不重要。两件事。顺便说一下,我的代码在集群环境中会发出错误,这可能导致真正的竞争条件。这将证明“watch”是安全的。 - Henry Leu
我已经使用这个额外的命令执行了你的代码:setTimeout(function(){ rc.get("inc",function(err,data){ console.log('inc value: ' + data); }); },3000);结果是[ 1, 'OK' ] [ 2, 'OK' ] [ 3, 'OK' ] [ 4, 'OK' ] [ 5, 'OK' ] [ 6, 'OK' ] [ 7, 'OK' ] [ 8, 'OK' ] [ 9, 'OK' ] [ 10, 'OK' ] inc value: 1我原本期望的是 inc value: 10 - Aebsubis

1
如果您想使用事务/原子MULTI操作,但希望使用共享连接进行操作,据我所知,您唯一的选择是使用LUA。我在redis中使用LUA脚本做很多事情,LUA的好处是整个脚本将以原子方式执行,这非常方便。但是需要注意的是,如果您的LUA脚本速度较慢,那么您将使redis变慢,从而影响到其他人使用您的服务器。此外,当使用LUA时,即使可以操作不同的键,也必须注意,如果在脚本中使用了多个键,则一旦释放就无法使用Redis集群。这是因为在使用集群时,键将分布到不同的Redis进程中,因此您的LUA脚本可能无法在单个服务器上访问所有键。无论如何,在发出MULTI命令时,Redis集群的问题都将是相同的,因为在集群上不允许MULTI设置不同的键。祝好!

j


LUA在这里绝对是一个解决方案,谢谢。现在不确定谁将获得赏金 =) - igorpavlov

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接