上文说到时效性的问题,要怎么解决呢?
redis是key失效通知的,这个正好是我们需要的啊。
这个功能经过我们的dba的压力测试验证,对性能的影响是非常的小的。
但是他的通知是pub/sub,不像消息队列那么可靠,如果网络连接有问题,错过就错过了。如果多个server都去监听这个事件,还是面临最后哪一个server来处理的问题。
问:redis的key一失效就会马上有通知的吗
答:不是马上,redis是每次随机判断n个key是否失效,命中了才会通知。如果命苦会等一段时间了。
问:redis可以针对某个前缀的key做失效通知吗
答:并不可以。不过还好,在系统中其他的key比较少。
看来还是需要一个比较频繁的定时任务去解决了。这个定时任务具体怎么做呢?
最开始也想和方案一中mysql的定时任务那样做,多台luna server去set 一个key,成功的就抢到任务去做。每次任务对redis的操作次数是:先10000个服务,再每个服务1次mget 10个实例=2w次操作,但是redis cluster只是支持同一个slot的mget,不同的item在同一个slot的概率小到可以忽略。实际的操作就变成了10000x20=20w个操作了。这样一个任务的粒度太大了。而且如何获取所有的10000个服务呢,keys命令吗,这个命令其实对于redis并不友好。
我们的做法是用redis的list数据结构,注册时候服务加入list,反注册的时候从list中移除。利用rpoplpush命令实现一个环形队列,来分配任务,降低任务的粒度。
image.png-
rpoplpush srv:queue 获取一个service,比如serviceA
-
smembers srv:list:serviceA 获取结点列表的keylist, 使用expire命令逐个判断结点是否存在(可使用pipeline)
-
对于不存在的结点,从结点列表中删除,为避免并发问题造成的影响,再做expire xx判断结点是否存在,如果存在,使用sadd命令将结点加入; 如果不存在,则生成下线事件
4 sleep 5ms,然后再次执行第1步
方案优化
用环形队列虽然任务的粒度降低了,但是对redis的操作数并没有减少。之前操作压力主要是2000tps的心跳,现在这个定时检查的tps居然更高。本质上其实是一样的,都应该是对item的心跳的key的超时检查。
所以redis这个环形队列里面放的数据改为节点数据,如果节点不存在了,遍历所有的服务,找到这个节点的所有服务,生成对应服务的下线事件。
方案再优化
找到一个实例节点有哪些服务并不容易,要遍历所有的服务,10000个服务就是10000次hget,恶劣情况下,一个机柜的出问题,可能影响100个实例。对应的操作就是10000x100=100w次hget,压力很大,当然节点的key失效时间是20s,100W/20=5w,压力确实有点大了。
方案继续优化
根据节点找到对应的服务,遍历的话确实很容易造成比较大的redis压力波动。那假如我们把这个对应关系存起来呢。空间换时间了,确实是个很好的买卖。那存起来的这个key永不过期吗,那就是会有很多垃圾数据,因为容器每次都是不同的实例。垃圾数据的清理就是很头疼的问题了。那还是要设置失效时间了,那就必然每次心跳都要加这个key的超时时间了。
这样心跳的tps的增加一倍了,那心跳就是2000x2=4000tps,这个还是可以接受的。
所以这就是最后采用的方案了吗?
可能并不是。注册和反注册的时候,都会操作好几个redis 的key,很难保证这些操作的完整性。
用redis的事务?
在redis cluster的情况下,怎么保证在同一个redis节点呢,当然我们用相同的hashtag。
redis事务的多个操作是不能有依赖关系的,但是我们的生成事件依赖于生成最大的事件id的,这个怎么办呢?
用redis的watch?
watch事件的最大id,类似乐观锁的+1,如果有冲突再重试。但是redis cluster默认的连接策略是不支持的watch的
使用自定义的redis的连接策略?
嗯,我们修改默认的redis的连接,每次拿到的就是hashtag对应的那个redis节点。
那还有不有更好的解决方案呢?也许就不会有下文了。。
网友评论