在 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
x
和e
, 但是e
是内存不够用时键被迫过期, 这种情况可能性不大,所以 e
可以不考虑.
另外 K
和 E
的区别, 我不了解.
开启该功能之后, 我们可以测试一下:
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 实例一定不能含有大量要过期的数据, 原因, 你们懂得.
网友评论