当前位置: 首页 > 科技观察

系统设计分布式计数器

时间:2023-03-20 20:38:20 科技观察

应用场景说到计数器,大多数人都不陌生。毕竟柜台应该太多了。小到一个博客系统的文章数,大到抖音视频的点赞评论数,淘宝的商品库存数等等。可以说,统计的目的就是放一个一个对象上的数字,这个数字用来表示一定的商业意义。通常,我们不一定需要显式地创建一个计数器。比如我们要统计商店里的宝贝数量,只需要写一个SQL语句,实时统计剩余商品的数量。这样就达到了最高的准确率,但是缺点是如果流量很大的话,会有明显的性能瓶颈。比如我们以抖音的点赞数为例。一个上百万点赞的热门视频,显然不可能每次都有这么大的点赞数。于是,这个时候,柜台的需求就凭空诞生了。计数器简单的理解就是一种帮助我们快速获取计数结果的机制。计数器用例:高性能获取计数值分布式计数器实现单机计数器的实现没什么好说的。每种编程语言都提供相应的数据结构。下面我们分析一下分布式计数器的实现方法。通常,我们有两种选择:MySQL计数器、Redis计数器。①基于MySQL实现计数器要使用MySQL实现计数器,我们可以单独建表。这个表主要有一个业务主键列,用来表示业务id(比如videoid),还需要一个count列来记录当前的count值。一个简单的MySQL表可以使用乐观锁来保证数据增加时的幂等性,如果执行失败则自旋重试。//首先selectcurrent_countselectcountascurrent_countfromxxxwhereid='xxx'//更新计数值updatexxxsetcount=current_count+1whereid='xxx'andcount=current_count实现起来很简单counter用mysql,如果业务数据也在mysql中,那么可以很方便的做跨表事务,保证整体数据的一致性。但是缺陷也很明显,因为`update`语句有行锁(即使id不是主键,也可能是间隙锁),那么在竞争激烈的情况下,可能会出现严重的性能下降.这时候可以考虑做一些性能优化:降低锁粒度。实现也很简单,就是同一个业务ID可以使用X条数据,每次更新随机更新一条数据,这样锁冲突的概率就降低到1/X.查询count值时,需要修改为查询相同业务ID的Sum(count)。②基于Redis实现计数器使用Redis作为分布式计数器也是一种常用的方法。与MySQL相比,Redis几乎没有性能问题(单机可以支持10wqps+),而且Redis内置了`IncrBy`操作,可以原子的实现计数的累加。但是,使用Redis作为计数器的问题之一是操作不是幂等的。例如调用`IncrBy`命令后收到网络错误,无法判断服务器执行成功还是失败。这导致你无法确定是否应该重试,最终导致计数结果出现偏差,典型的两军问题。为了解决这个问题,最常见的方法是使用LUA脚本,在每次执行INCR时,同时使用`SETNX`设置一个值。LUA脚本保证SETNX和INCR操作同时成功或失败(原子性),所以当你收到错误返回信息时,是否重试只是判断对应的KEY是否设置成功。举个例子:某视频点赞,假设点赞的businessid为1000,那么LUA脚本的执行逻辑是`SETNX1000true`+`IncrBycountKey`同时成功。最后,使用Redis计数器来防止热KEY。Redis虽然可以承受大量请求,但毕竟是单点存储(读写分离)。所有写请求仍然发送到同一个节点。需要评估单个节点的写QPS,一定要防止过热的KEY出现。Tradeoff:一致性和可用性通常,计数器和管道是分开计算的,由于异构存储,可能会存在一定的不一致性。这个时候就需要权衡业务对不一致的容忍度。通常,我们需要权衡可用性和一致性之间的冲突。如果一致性很重要,可以考虑使用MySQL模式将业务数据和计数器合并在同一个事务中保证强一致性,或者引入分布式事务保证异构存储的一致性,或者使用Redis计数器+LUA脚本模式等。但是,需要注意的是,不管是什么模式,如果一致性高,必然会在性能和可用性上打折扣。如果业务没有强烈的需求,没必要弄的那么复杂。您可以引入定期回溯脚本并进行定期更正。记住,如果你不考虑业务结构,你就是在耍流氓。结束语在我们的业务开发工作中,经常会遇到柜台的需求。一开始觉得很简单,不就是RedisIncr吗?事实上,当业务变得复杂,当数据量变得庞大,当对计数器的一致性要求变得更高时,这一切都会在演进中变得复杂。难以处理。以上是我在日常工作中总结的两种常用且有效的分布式计数器实现。如果你也在工作中使用它,你也可以尝试一下。如果你暂时没有使用过,相信适当了解后,在面试和日常工作交流中会有用。