高并发文章浏览量计数系统设计

最近由于我的网站的文章浏览量计数在Chrome浏览器下有BUG,因此打算从新实现这个功能。前端

本来的实现很简单,每次点击文章详情页的时候,前端会发送一个GET请求articles/id获取一篇文章详情。这个时候,会把这篇文章的浏览量+1,再存进数据库里。redis

这个实现本来能够实现这个功能,可是后来我才发现,我犯了一个很致命的错误:在GET请求的业务逻辑里进行了数据的写操做!sql

原则来说,GET请求应该具备幂等性,即短期内同时两个如出一辙的GET请求,返回的结果也应该是同样的。而我本来的实现就破坏了GET请求的幂等性。数据库

刚好,在Chrome浏览器里,个人文章详情页会发送两次GET请求。这疑似Chrome浏览器和nuxt服务端渲染之间的一个BUG,目前尚未定位到具体缘由。后端

但不管如何,后端应该是能够避免这样的BUG,即便某用户短期内请求两次或者屡次,也应该只增长一次浏览量计数。浏览器

因为最近在学习高并发方面的知识,因此这里也考虑一下,若是一个高并发的文章浏览量计数系统,应该如何设计?缓存

先来理一下需求。多线程

需求

  1. 用户能够是匿名的,不须要登陆
  2. 每当一个用户点击了一个文章的详情页面,这个文章的浏览量应该+1
  3. 用户应该能当即看到本身点击文章后浏览量+1的反馈
  4. 浏览量这个数据存在Mysql和ElasticSearch里面,要最终一致(不要求强一致)
  5. 做者可能在后台编辑文章,而后保存文章。若是在这期间有浏览量的增长,保存文章的时候不该该覆盖掉这段时间的浏览量增量。
  6. 应该在服务端对用户的请求去重,防止用户不断刷新或者使用爬虫不断请求某个API(建议经过IP)
  7. 要过滤掉百度和谷歌的爬虫请求(根据User-Agent头判断,能够先不作)
  8. 要高性能地实现“查看浏览最多文章列表”的功能。
  9. 尽量优化性能,知足多个用户的高并发需求。

设计思路

若是要知足高并发,那首先考虑用异步和缓存。因此考虑使用多线程加Redis的解决方案。并发

请求流程:异步

  1. 用户点击某篇文章详情页
  2. 前端发送一个PUT请求/articles/{id:\\d+}/view
  3. 后端使用线程池执行一个异步任务,当即返回给前端200响应。
  4. 前端获得200响应后,当即把当前文章的浏览量+1,知足需求3。

请求流程.png

后端主要逻辑:

后端的主要思路是暂时把增长的浏览量(假设某篇文章为n)放进Redis里,而后每隔一段时间刷新到Mysql数据库和ElasticSearch存储里,让这篇文章的浏览量在现有的基础上加n,而后把Redis这篇文章的浏览量清零。

  1. 后端首先判断redis里时候有没有当前ip对这篇文章的浏览记录,这个key为:isViewd:articleId:ip。若是有,就说明以前浏览过,就什么也不作,直接返回。若是没有,就加上这个key。时间能够设置为1小时过时,防止占用过多内存。这里使用Redis的string类型。
  2. 若是第5步的结果是没有,那就在Redis里给这篇文章的浏览量+1。Redis的这个支持原子操做,因此不用担忧并发问题。key为viewCount:articleId,value为缓存的浏览量。完成后当前线程任务就结束了。这里使用Redis的string类型。这些key应该没有过时时间。
  3. 弄一个定时任务,好比每5分钟,去Redis里拿缓存的浏览量,拿到后就更新到数据库和ElasticSearch里,并把Redis的数据清零。为了防止并发带来的问题,这里应该是拿到m,就在Redis里减去m,而不是直接设置为0。
  4. 为了节约内存,应该删除没必要要的key,按照业务逻辑来看,若是一篇文章长时间没有人浏览,可能这篇文章比较“旧”了,咱们能够考虑删除它在Redis里面的key。因此咱们能够在第6步,每次在Redis里进行浏览量+1操做时,记录下一个时间戳。因此Redis可使用hash类型,一个字段存最后操做时间,一个字段存浏览量。而在第7步里,咱们能够顺便删除掉最后操做时间小于十天前的key。
  5. 保存更新文章的时候,应该只更新其它字段,而不更新浏览量这个字段。或者执行一遍第7步的逻辑。因为Redis加减操做的原子性,这里不用担忧并发问题。若是当前线程把一篇文章的浏览量在Redis里减了m,那定时任务线程应该获得的是减了m以后的结果,因此数据会是一致的。
  6. 关于需求8,在并发量不算特别大的时候,咱们仍是去取数据库里面的数据,根据数据库里面的浏览量来排序,只是能够在应用里面给它加一个缓存,缓存时间应该与第7步定时任务一致,这里设置为5分钟。

若是并发量特别大,能够考虑不把浏览量存在数据库里,而仅存在Redis里,这样能够获得近乎实时的浏览量存储,并且需求8排序也是实时的(使用zset),但这样可能会耗费大量的内存资源。

后端逻辑.png

后记

虽然最后权衡了并发量和复杂性,个人我的网站的文章浏览逻辑并无彻底按照上述设计思路来作,但上述思路是我对一个高并发文章浏览量计数系统设计的思考,之后若是有机会能够写一个开源的版本。

可能实现起来会更复杂,根据并发量的不一样,代码也会有一些差异,以上思路仅供参考。

相关文章
相关标签/搜索