美文网首页
模拟跨进程/服务器的 Timer

模拟跨进程/服务器的 Timer

作者: gruan | 来源:发表于2020-05-21 16:32 被阅读0次

    AsNum.Throttle.Redis.RedisCounter 中, 由于先入为主的观念, 让我忽略了一个问题:
    跨进程/跨服务器 下, 如何同步各个客户端的 周期 在 同一个时间段内.

    之前在 DefaultCounter (单进程) 中, 我使用了 Timer 来定期重置计数, 没有问题, 因为 DefaultCounter 是设计用于单进程模型的. 但是 RedisCounter 是设计用于多进程/多服务器的, 这样一来就会发生一个问题:

    • 假如有 A, B 两个客户端, 周期为10秒.
    • A 在 XX:12:00 开始,
    • B 在 XX:12:05 开始,
    • 如果使用 Timer , 则 A 客户端会在 XX:12:10 的时候进行清零操作...
    • 由于是共享数据, B 客户端也被迫进行了清零...
    • 如果有 N 个客户端在不同时间启动, 那个这个 Counter 就几近于崩溃了...

    好了, 问题描述清楚了, 要解决这个问题, 看来是要找个可跨进程的 Timer 了, 嗯, 还要跨服务器.

    可是, 肿么可能?? 还跨服务器, 跨进程的 Timer 都没有...

    那即然没有这样的东西, 那就不要费劲巴拉的去挠头皮了.

    Redis key 过期通知

    Timer 达不到要求, 是因为不能跨进程, 那么我们可以借用 Redis 的键的过期通知来模拟 Timer, 只是精度稍稍有点粗糙而已.

    过期通知, 本质上就是 publish / subscribe, 只不过, 这个发布者是 Redis 本身.

    要使用该功能, 需要开启 notify-keyspace-events 配置, 由于该功能是要消耗 CPU 的, 所以默认是关闭该功能的.

    查看 notify-keyspace-events 是否开启:

    config get notify-keyspace-events
    

    如果返回的是空字符串, 那就是没有开启该功能.

    notify-keyspace-events 配置对应的含意:

    解释
    K Keyspace events, published with __keyspace@<db>__ prefix.
    E Keyevent events, published with __keyevent@<db>__ prefix.
    g Generic commands (non-type specific) like DEL, EXPIRE, RENAME, ...
    $ String commands
    l List commands
    s Set commands
    h Hash commands
    z Sorted set commands
    t Stream commands
    x Expired events (events generated every time a key expires)
    e Evicted events (events generated when a key is evicted for maxmemory)
    A Alias for g$lshztxe, so that the "AKE" string means all the events.

    除了 K E 之外, 还有一个 A, A的意思是 g$lshztxe 这些个选项的简写.

    这里, 我们只用到键过期的通知:

    config set notify-keyspace-events KEx
    

    xe, 但是e是内存不够用时键被迫过期, 这种情况可能性不大,所以 e 可以不考虑.

    另外 KE 的区别, 我不了解.

    开启该功能之后, 我们可以测试一下:

    psubscribe __keyevent@0__:*
    

    这里用的是 psubscribe 而不是 subscribe , 是因为我一开始我也不知道 Redsis 会发布消息到哪个 channel 中, 所以用了通配符 *, 然后随便添加一个值到 Redis 中去, 在设置这个键的过期时间:

    1) "pmessage"
    2) "__keyevent@0__:*"
    3) "__keyevent@0__:expired"
    4) "test"
    

    通过上面的输出, 可以看到键过期后, 会发布一个 channel__keyevent@0__:expired 的通知. 那个 test 就是过期的 key.

    模拟 Timer

    那么知道了上面这些, 我们就可以模拟跨进程/服务器的 Timer 了.
    思路是这样的:

    • 在 increment 的时候, 如果结果为1, 那就说明是第一次设置该值. (如果不存在, 则 increment 会把这个值为初始为 0, 然后加1)
    • 如果是第一次设置该值, 则设定过期时间; 否则静待该值过期.
    • 每个进程都订阅 __keyevent@0__:expired 这个 channel.
    • 当收到该 channel 的消息时, 比对消息的值 (key 的名称), 如果是预设的 key, 则代表时间已到.
    public override int IncrementCount()
    {
        try
        {
            var n = (int)this.db.StringIncrement(this.ThrottleName, flags: CommandFlags.DemandMaster);
            if (n == 1)
            {
                //只有第一次时, 才对该值做 TTL
                db.KeyExpire(this.ThrottleName, this.ThrottlePeriod, CommandFlags.DemandMaster);
            }
            return n;
        }
        catch
        {
            this.db.StringSet(this.ThrottleName, 0, flags: CommandFlags.DemandMaster);
            db.KeyExpire(this.ThrottleName, this.ThrottlePeriod, flags: CommandFlags.DemandMaster);
            return 0;
        }
    }
    
    
    
    protected override void Initialize()
    {
        this.subscriber.Subscribe("__keyevent@0__:expired", (channel, value) =>
        {
            if (value == this.ThrottleName)
            {
                this.ResetFired();
            }
        });
    }
    
    4进程的 Timer 效果

    就是这么简单, 就是这么帅...

    注意

    由于是订阅了 key 过期的 channel , 所以用作模拟 Timer 的 Redis 实例一定不能含有大量要过期的数据, 原因, 你们懂得.

    相关文章

      网友评论

          本文标题:模拟跨进程/服务器的 Timer

          本文链接:https://www.haomeiwen.com/subject/ioqtahtx.html