场景描述
在BBS系统中,为了在帖子列表中显示每个帖子的回复数量,通常会把回复数作为一个字段存储在某处(Redis或Post表中),避免在请求时count的额外消耗。那么,如何更新这个字段才最为合理?
增量方案
很多人会下意识地给出如下方案:
任何人回帖的时候,读取之前的回帖数,加1,再写回去。如果有人*帖删**,则把这个数字减1再写回去
long count = post.getReplyCount()
count++ // or count--
post.setReplyCount(count)
post.save()
这看起来不错,足够简单易实现。
但网站运行一段时间之后,你发现,很多帖子的回帖数都跟实际数量对不上了。发生了什么?
从开头的例子开始,逐渐给它加点料。
场景描述(2)
这个BBS异常火爆,每个帖子都同时有大量的用户在阅读和回帖。并发数量庞大。
这个很简单,很多人直接就想到了:上面的方案里,读和写的并行顺序问题会导致实际写入的数值比帖子总数少。
这里我们并不打算提多线程或者数据库相关的任何概念(毕竟我甚至都没有说这个值是不是存在内存里了)。只说本质:无论你用什么介质存储这个值,你都需要一些额外的手段来保证多读多写环境下的+1操作能够正确执行。
我们继续
场景描述 最终版
这个BBS用户数量上升后,很多帖子因为内容问题被人投诉,需要被删除。
这个也不难,甚至不需要解决:因为我们之前在用户自己删除帖子的地方已经加了count--的代码。只要把那个代码复制到用户投诉删除的逻辑中就行了。
等等。。。这么搞不行,虽然投诉删除和自己删除是不同的逻辑,但他们都是删除,应该统一控制count--。重构代码后,投诉删除和用户自己删除变成了同一个方法调用,count--被复用了。很棒。
场景描述 最终版(1)
这个BBS逐渐发生了恶意投诉的事件,很多帖子被恶意投诉删除了,于是产品经理认为,投诉不能删除帖子,只能隐藏,以免申诉时无法找回。
这个有点烦。。不删除只隐藏,无法触发删除逻辑了,于是只能编写代码在隐藏功能中也去执行这个count--。
虽然完成了任务,但聪明的你已经感觉到事情有点不对劲了。这个count++--以后不会到处都是吧?。。。
场景描述 最终版再也不改了
产品经理让你在产品中增加后台审核功能、延迟发帖功能、伪装成帖子的广告位功能、用户注销账号后的级联*帖删**功能、只看楼主功能、只看自己功能。。。
场景描述 最终版再也不改了(1)
MySQL、Redis从一个变成了很多个集群
后端服务从单进程变成了多pod
场景描述 最终版再也不改了(2)
网站App端是不同的部门,和你们公用同一套数据库但是用最好的语言php另外写了一套后端。
(请务必把每个场景都大概过一遍脑子,看看为了维护正确的ReplyCount,又产生了多少工作量)
终于,你的count++--,遍地开花了。你不仅需要
* 在自己的所有功能之间不断重构,让涉及到Reply的功能全部不要忘了更新ReplyCount值
* 在进程崩溃重启之后恢复异步更新ReplyCount数值的任务
* 不停的Review其他人的代码,避免数据库脏读脏写造成count可能算错的情况
* 不停的Review其他人的代码,避免多进程导致多写时序出问题覆盖正确值
而且你甚至需要发个公司公告,让PHP项目组的人也做同样的事情
有人说,你总改需求不讲武德。对,产品经理什么时候讲过武德?只有他们打不过开发的时候。
说正经的。的确,客观上造成现在这个局面的,是需求逐渐复杂了。但我们的设计一开始真的就没有问题吗?
让我们再来看一遍你最后要做的那些事情:
* 在自己的所有功能之间不断重构,让涉及到Reply的功能全部不要忘了更新ReplyCount值
* 在进程崩溃重启之后恢复异步更新ReplyCount数值的任务
* 不停的Review其他人的代码,避免数据库脏读脏写造成count可能算错的情况
* 不停的Review其他人的代码,避免多进程导致多写时序出问题覆盖正确值
* 发个公司公告,让PHP项目组的人也做同样的事情
它们有什么共同特点?
你被这个count可能算错的问题给困住了!
你无比担心这个count时间久了就会被更新错掉。它特么有太多可能性被更新错了。你现在无法信任任何人,因为任何人只要写一个新功能不遵循你的设计,甚至只要断了一下网、重启了一个后端进程、redis挂了5秒钟,就可能导致count被算错一次。
而可怕的是,一旦一个count算错了,那它就再也不可能回到正确值了!
吓你们的
其实没那么夸张,你也不会蠢到死命坚持用这个方案来完成后面所有的设计。
这个例子只是想带你一起思考一个问题:
“ 如何判断设计方案的好坏? ”
这是一个开放命题,没有标准答案。但今天只重点关注一个评价标准:心智负担。
上面的例子中,为什么最后大家都会很明显地认为这个方案已经不合适了呢?
因为这个方案太累人了。别忘了,这只是一个小小的回帖数功能而已。
使用增量来算出ReplyCount成本,非常低。初级程序员也可以几分钟就搞定。但要保证ReplyCount算对,而且在各种场景下都要算对,几乎耗费了几百倍的精力。
这一切都是增量惹的祸。
增量的性质:单次计算量少。但强依赖于初始状态和之后的每一次更新计算。任何一次崩溃中断、同步错写,或者就是单纯的程序bug,导致的更新错误,都会让整个链条永远错误下去。
因此,选择增量,相当于选择了一个初始实现很简单,但要花费大量精力维护它在各种场景下永远永远不要出错的方案。低初始成本、高维护成本、低容错、无自动修正机制、高心智负担。
几乎所有计算汇总值的实现(无论是仓储系统的库存数量,还是IM的消息未读数、CRM中的单客户累计订单额等),只要你采用了增量的方式,最后一定都会陷入这样的境地。
设计原则:高心智负担的设计不是好的设计
像这种开始写起来很简单,但可预见的将来会变得无比累人的设计,就不是一个好的设计。
结论
一、让人不省心的设计不是好的设计
二、增量方案太容易发生错误,又无法自我修复,不让人省心
三、所以不要只用增量来计算汇总值
所以你想一下,
为什么财务要月底对账?
为什么仓储要每天盘库?
你小时候过年三姑给你200、二姨给你300,你明明能加得过来,为什么晚上睡觉之前要把钱拿出来再数一下?
因为人天然就不相信增量!
有人会问那你说了半天什么才是最优方案?
文章太长了,先写到这儿,关注我,以后会更新最优方案哦~