Redis - 简单队列读取器/写入器的正确方法 - StackExchange.Redis

时间:2017-09-26 16:04:01

标签: redis stackexchange.redis

我正在寻求使用StackExchange.Redis实现一个简单的分布式工作队列系统。

我理解没有BLPOP等的原因,但是因为我正在努力的界面基于重复的TryRead调用超时。

我对下面的内容有所了解,因为我在处理程序中取消订阅,并设置一个标志来取消超时。有没有机会可能错过?是否有不同的方法来实现这一目标?

    public string TryRead(string queueName, TimeSpan timeout)
    {
        string result = null;

        var chanName = $"qnot_{queueName}";
        var done = new ManualResetEvent(false);

        void Handler(RedisChannel chan, RedisValue val)
        {
            _sub.Unsubscribe(chanName, Handler);
            result = _database.ListRightPop($"qdata_{queueName}");
            done.Set();
        }

        _sub.Subscribe(chanName, Handler);
        done.WaitOne(timeout);

        return result;
    }

    public void Write(string queueName, string text)
    {
        _database.ListLeftPush($"qdata_{queueName}", text);
        _sub.Publish($"qnot_{queueName}", "");
    }

如果队列中存在现有项目(并且未添加任何新项目),则上述版本将始终超时并返回null。以下版本现在首先检查现有数据,这是有效的。但它有一个错误,一个竞争条件:如果第一次读取检查返回否定,然后推送某些内容并发送通知,那么我们订阅并等待超时。

    public string TryRead(string queueName, TimeSpan timeout)
    {
        var dataName = $"qdata_{queueName}";

        var result = (string)_database.ListRightPop(dataName);
        if (result != null)
        {
            return result;
        }

        var chanName = $"qnot_{queueName}";
        var done = new ManualResetEvent(false);

        void Handler(RedisChannel chan, RedisValue val)
        {
            _sub.Unsubscribe(chanName, Handler);
            result = _database.ListRightPop(dataName);
            done.Set();
        }

        _sub.Subscribe(chanName, Handler);
        done.WaitOne(timeout);

        return result;
    }

我可以在循环中执行RPOP,但这似乎绝对很糟糕。其他人做过类似的事情吗?

1 个答案:

答案 0 :(得分:0)

我最终得到了这个,但是我仍然会以一种可行的方法欢迎其他答案:

    public string TryRead(string queueName, TimeSpan timeout)
    {
        var timer = Stopwatch.StartNew();
        var dataName = $"{_keyPrefix}qdata_{queueName}";
        var chanName = $"{_keyPrefix}qnot_{queueName}";
        var done = new AutoResetEvent(false);
        string result;

        // subscribe - sets the 'done' flag when a new item is pushed
        void Handler(RedisChannel chan, RedisValue val)
        {
            done.Set();
        }

        _sub.Subscribe(chanName, Handler);

        do
        {
            // try to read right away (before waiting), in case there was data already there
            result = _database.ListRightPop(dataName);
            if (result != null)
            {
                continue;
            }

            // there wasn't an item right away, so wait for the timeout to expire
            // or the subscription to be fired.  if it fired, try the read again
            var remainingTime = timeout - timer.Elapsed;
            if (remainingTime.TotalMilliseconds <= 1.0)
            {
                break;
            }
            if (done.WaitOne(remainingTime))
            {
                result = _database.ListRightPop(dataName);
            }
        } while (result == null && timer.Elapsed < timeout);

        _sub.Unsubscribe(chanName, Handler);

        return result;
    }

修改:已更新w / AutoResetEvent并已从处理程序中删除Unsubscribe。对于那些发现这一点的人来说,这似乎对我来说是一个替代单个阻塞读取的替代品,但它不会是推荐的方法。我只是使用它,因为我希望与其他队列实现保持一致,并且正在努力使用这个特定的TryRead签名。