`

在redis使用过程遇到的一些问题的总结

 
阅读更多

     tpn(taobao push notification)在使用redis计算消息未读数的过程中,遇到了一系列的问题,下面把这个过程整理了一下,也让大家了解这个纠结的过程,供大家以后使用redis或者做类似的功能时进行参考
     redis在tpn里面主要是用于计算移动千牛(Android、IOS)上的消息未读数。tpn的未读消息数是基于bizId维度的,即同一个bizId(每条消息的业务id,如果商品id、订单id等),即使有多条消息,未读数也只能算1。因此在接收消息,计算移动千牛未读数的过程中,就需要对bizId去重,这个去重的功能就是通过redis来实现的。随着消息量的不断上涨,这个基于redis的去重方案也不断变化。

一、基于redis Set结构的未读数计算
     前面说到的tpn未读数计算的最大特点就是基于bizId去重,在java里面,我们很容易想到利用HashMap或者HashSet来判重,因此最初tpn就是利用redis的Set结构来进行判重。主要利用了redis set结构的这两个命令:SADD和SCARD
SADD key member  [member....]:将一个或多个 member 元素加入到集合 key 当中,已经存在于集合的 member 元素将被忽略。假如 key 不存在,则创建一个只包含 member 元素作成员的集合。 如果member元素不在集合里面,则返回1;如果member元素已经存在于集合当中,则返回0。
SCARD key:返回集合 key 中元素的数量。
     有了这两个命令,计算未读数的步骤就是这样的:

     tpn会为用户保留7天内的消息,也就是说保存到redis set结构中的bizId失效时间是7天,同时用户在查看消息后,就会把其对应的redis set清空(即如果一个用户连续几天都不查看千牛的消息,那么其对应的redis set集合里面就会保存大量的bizid)。tpn总共有6台redis机器,每台机器上部署5个redis实例,每个实例的maxmemory设为1G,总共30G的内存用于存放消息bizId。在tpn的早期,由于用户量不多,消息量也不大,redis的内存完全可以存放7天内的所有消息bizId,因此这个方案work的很好。但随着全网大多数活跃卖家开始使用千牛,tpn的消息量也随之暴涨,越来越多的消息bizId给redis带来了极大的压力,在消息高峰期,tpn的日志里会有大量的redis timeout异常(tpn使用jedis,配置的timeout是300ms),经过分析,主要是由下面原因造成的:

  1. 缓存失效造成的超时:前面我们提到了,tpn的每个redis实例的maxmemory设置的是1G,因为bizId越来越多,因此很快每个redis 实例的内存就超过了maxmemory。而redis在处理客户端请求时,如果发现当前内存的使用量已经大于等于maxmemory,就会去失效部分过期的缓存,直到内存使用量小于maxmemory。很明显这个失效缓存释放内存的操作会影响redis的rt。在消息高峰期,redis实例的内存使用量一直再maxmemory附加徘徊,造成redis在应对大量请求的同时,还要不停地失效缓存释放内存,造成频繁超时。

      因为bizId太多,而redis内存不够,所以造成redis请求大量超时,最简单地办法就是加机器,部署更多的redis实例来存储越来越多的消息bizId。初步估计了一下,要完全把7天内的所有消息bizId都保存到内存中,需要高达上百G的内存:交易消息和商品消息是tpn最主要的两类消息,因为目前全网大多数活跃卖家都使用了千牛,为了去重,tpn需要把全网7天内所有新增的交易id和商品id都保存到redis内存中,换句话来说,也就是要用内存来保存7天内tc和ic新增的所有id。tpn基本不可能申请到这么多的redis机器,就算有这么多的redis机器,部署维护成本也是巨大的。就算不用redis,使用tair的rdb,这个陈本仍然是不能接受的。
     在移动千牛客户端,推送没有正常到达的情况下(比如长连接断开的时候),是依赖客户端在发现长连接断开以后调用messagecount.get接口来获取到消息未读数,然后促使用户手动获取最新的消息。当redis的内存使用量接近极限时,调用redis的sadd、scard命令很容易就timeout了,因此不能正确地计算出消息未读数,就会造成用户不能及时获取到最新的消息。     
     总的来说,redis的内存容量不足以容纳越来越多的业务消息bizId,造成大量redis请求超时,不能正确地计算消息未读数。因此需要对上述方案进行优化。

二、redis用于消息去重判断,tair存放未读数消息数的方案
     根据上面的分析,当redis内存使用量达到了上限时,很容易发送timeout,同时redis内存使用量会之所以会很快地达到上限,主要是因为不活跃用户的set结构里面保存了大量的bizId。在不能快速增加redis机器的前提下,最简单地方法就是在夜间重启redis。重启redis会带来一下影响:
所有用户保存在set里面的消息bizId全部被清空了,就会造成误判:即对同一个bizId的消息重复提醒用户有新消息。但这个并不会对用户造成太大的影响:因为活跃用户会及时地来查看消息,所以活跃的set结构基本都是空的;而非活跃用户的redis set结构虽然有很多消息bizId,但是因为其是不活跃的,就算被清空,很快又会有新的bizId存放进去,但认为是不活跃用户,对这种情况基本无感知。
因为set结构被清空,所以所有用户的消息未读数也被清空(通过scard命令来计算未读数)。根据前面的分析,在消息推送不能正常达到的情况下,正确的未读数会促使用户主动地来获取最新消息,所以基本不能接受重启redis的时候,清空用户的消息未读数
     因为不能接受随意清空用户的消息未读数,所以我们不能定期重启redis来释放内存。但是如果我们把消息去重和计算未读数分开,即redis的set结构只用于判断一条消息是否是新消息,是否需要增加未读数,而把未读数保存在其他的地方,如果tair之类的,那我们是不是就可以定期重启redis了呢?因此我们得到了下面的方案:
继续是用redis的set结构来判断一条消息是不是新消息,是不是需要增加消息未读数
不再使用redis的scard命令计算消息未读数,而是采用基于tair的计数器来计算消息未读数,即如果通过redis的set结构判断出是新消息,则对保存在tair里面的未读数计数器执行incr unReadCountKey 1。

     这样一来,redis就只用于对消息bizId去重,而不再用于计算消息未读数,消息未读数单独保存在基于tair的计数器当中。因此我们就大胆地定期在夜间重启redis了。这个方案成功work了一段时间,但过了一段时间后,应用在请求redis的时候又开始是不是抛出大量的timeout exception。分析了一下,问题还是处在redis内存上:
虽然可以通过定期重启redis来释放内存,但是redis内存的增加的速度是不可预期的,我们并不能每次都能在内存使用达到极限前重启redis
有时候虽然redis的整体内存使用量还没有达到极限,但是如果一个用户的set结构里面的bizId太多了,scard命令仍然会timeout
     所以这个方案还不是一个最佳的方案,仍然需要通过更好的办法来降低redis的内存使用量

三、基于redis的bloomfilter的消息去重方案
     从方案一到方案二,我们一直想解决的就是如何用最小的内存来判断一个消息bizId是不是新的bizId,即一个消息bizId是不是已经存在了。以最小的内存来实现判断操作,很容易就联想到bloomfilter。但是在这个场景,我们不能简单地使用bloomfilter,先来计算一下“最直接”地使用bloomfilter需要多大的内存:bloomfilter的所占用的内存由bitSize决定,而根据公式:
                   bitSize =  (int) Math.ceil(maxKey * (Math.log(errorRate) / Math.log(0.6185)));
     我们为每个用户的每个消息类型创建一个bloomfilter,以500万用户,每个用户订阅了10个消息类型,那么这个用于去重的bloomfilter所占用的内存总量是:
         totalMemory(G) = 5000000*10*Math.ceil(maxKey * (Math.log(errorRate) / Math.log(0.6185)))
     这个totalMemory的大小就取决于maxKey和errorRate,保证errorRate不变的前提下,bloomfilter 的maxKey越大,bloomfilter所需要的内存也就越大。那我们估算一下使用bloomfilter,需要多少内存。
以商品消息和交易小为例,不同的卖家,7天内的消息数从几个到几万个不等。最小的是7天只有几条消息,最多的7天内有7万多条。就算取个1000的评价值,这5000w个bloomfilter的内存消耗也在上百G,这明显行不通。
     但是,tpn的消息未读数还有一个业务特点就是,当一个用户的某个消息类型的未读数已经超99了,就不再显示具体的数字,而是显示成99+,同时一个用户的消息未读数超过了99,那么其实他自己对消息未读数的敏感性也不高了,即就算有一条消息不是新消息,但是仍然给未读数+1了,用户也察觉不出来。     
     因此,在上面的公式里,我们可以把每个bloomfilter的maxKey设为100,那这样一来,所占用的内存就是一个十分能够接受的数字了:设errorRate=0.0001,maxKey=100,那么上面的5000w个bloomfilter只需要11G的内存,很明显,这不是一个完全可以接受的内存消耗。
     这样一来,我们就得出下面这个基于redis bloomfilter去重方案:

  1. 通过redis的setbit命令来实现一个远端的bloomfilter,具体可以参见这个例子:https://github.com/olylakers/RedisBloomFilter/blob/master/src/main/java/org/olylakers/bloomfilter/BloomFilter.java
  2. 每次来一条新消息,通过redis的bloomfilter来判断这是不是一条新消息
  3. 如果是,则对tair中的未读数计数器+1
  4. 用户每次读取消息后,则清空对应的bloomfilter

    这样一来,终于我们可以通过能接受的内存来实现未读数的计算,不再要每天担心redis是不是内存不够用了,应用又频繁抛timeout exception了

四、诡异的connection broken pipe
     在方案三上线以后,我认为这些redis应该会消停了,redis运行一段时间后,的确再也没用timeout exception了,但是在运行一段时间后,tpn在向redis执行请求时,往redis写入命令时会报这个异常:
java.net.SocketException: Broken pipe。我们知道,如果一个socket连接已经被远端给close掉了,但是客户端没有察觉,仍然通过这个连接读写数据,那么就会产生Broken pipe异常。因为tpn使用jedis,通过common pool来实现jedis的connection pool,我第一反应就是tpn没用正确使用jedis的connection pool,没有销毁掉broken的redis connection,而是已经重新把归还给了connection pool,或者是jedis的connection  pool有bug,造成了connection泄露,导致ton在往一条已经往一条已经被close的连接写入数据。但是仔细检查了一遍tpn的代码和jedis connection pool的代码,发现没用什么问题,那就说明有些redis是真的被redis服务端给关闭了,但是jedis 的connection pool没有发现。
     因为客户端的jedis pool没有问题,那么基本上可以确定的确是redis server端关闭了一些连接。首先怀疑的就是tpn的redis 配置出错了,错误地配置了redis.conf里的timeout 配置项:
首先怀疑的是不是tpn的redis配置不多,造成因此就去查看redis的相关代码。redis的配置文件redis.config里面有timeou这个配置项:
          # Close the connection after a client is idle for N seconds (0 to disable)          timeout 0
   检查了下tpn 6台redis上的所有配置文件,发现都没有配置这个选择,但是tpn部署了两个版本的redis,redis-2.6.14和redis-2.4,结果在redis-2.4里面,如果没有配置这个值,redis就会使用默认的值,5*60(s),而redis-2.6.14的默认值是0,即disable timeout,同时又去查看了下jedis common pool的设置,发现minEvictableIdleTimeMillis=1000L * 60L * 60L * 5L(ms),即一个redis连接的空闲时间超过5个小时才会被connection pool给回收。很明显,就是因为客户端和服务端的connection idle time设置不一样,造成了connection被一端关闭了,但是另一端没有感知,所有造成了broken pipe。解决办法就是把redid-2.4升级到redid-2.6.14。


五、总结
     从方案一到方案三,我最大的感触就是,在解决问题,优化方案的时候,不能仅仅执拗于技术本身,而是要联系业务思考。这个redis的bloomfilter的想法我很早就有了,但是我之前一直没有想到tpn未读数消息数只显示99+这个业务逻辑,而是一直想如何通过降低消息bizId的长度来尽可能地去节省内存,结果越想越复杂,然后就没有然后了。。。。
     

     

  • 大小: 94.4 KB
  • 大小: 93.9 KB
  • 大小: 92.3 KB
分享到:
评论
2 楼 olylakers 2014-07-18  
qianshangding 写道
看了下官方文档,scard命令的时间复杂度应该是O(1)吧?求解释

scard是O(1)的,这里应该是笔误,写错了,是因为我们有个场景还会去调用SMEMBERS获取里面所有的bizId,这个是O(n)的,所以就会超时,这里写着写着就写串了。。。多谢提醒
1 楼 qianshangding 2014-07-18  
看了下官方文档,scard命令的时间复杂度应该是O(1)吧?求解释

相关推荐

    redis4.0.x集群创建及问题处理

    使用自己购买的阿里云轻量级服务器模拟了redis集群,文档花了一晚上整理,解决遇到的问题写的详细,但搭建过程比较简洁,一点不啰嗦,着重于实操,后续也补充了许多注释进去,包含截图。主要写了以下几点: 1.redis...

    leetcode题库-learn:redis缓存activiti工作流leetcode算法并发vueLinux内核MQ

    总结了使用vue过程中遇到的问题(如、概念理解、) 【基础】 【大数据】 【安全】 【并发】目录下为学习并发的总结 (深入到jvm指令) 【数据库】目录下为使用数据库的一些总结 【趣唠Linux操作系统】目录下为学习...

    使用Redis实现延时任务的解决方案

    最近在生产环境刚好遇到了延时任务的场景,调研了一下目前主流的方案,分析了一下优劣并且敲定了最终的方案。这篇文章记录了调研的过程,以及初步方案的实现。 候选方案对比 下面是想到的几种实现延时任务的方案,...

    南航移动Redis-Cluster趟坑记

    在巨大的利润驱动下,这些公司都全力打造各自的系统以适应互联网市场发展的需要,而且在此过程中各个系统还不停地接受着亿万网民的检验。经历过千锤百炼后,那些“名门大厂”都纷纷总结出“高可用,高可靠,高并发下...

    细细品味架构·分布式Redis解决方案Codis(第9期)

    1、本期内容 1.1 版权申明 1.2 内容详情 1.2.1 相关概念简介 1.2.2 一致性的重要 1.2.3 Codis 的使用经验 1.2.4 分布式数据库和架构 1.2.5 现场答疑(Q&A) 2、知识扩展 ...2.5.8 使用过程中遇到的问题

    基于spark+drools+kafka+redis的大数据实时风控系统.zip

    在学习的过程中,我遇到了一些挑战,比如配置文件的理解和注解的正确使用,但通过查阅官方文档和阅读相关书籍,我逐渐解决了这些问题。最终,我能够独立地使用Spring Boot开发Web应用程序,并运用其优秀的特性提高了...

    Redis集群搭建全记录

    本文给大家总结了redis集群的概念等基础知识,以及个人在搭建redis集群是所遇到的问题及解决方法,非常的详细,有需要的小伙伴可以参考下

    使用memcached基于hibernate之上的高效数据库缓存架构方案(含源代码)

    该系统已经应用在舍得网上了,没有发现大问题,本人也相信该系统已经足够强大,应付数百万IP/天的应用都不是问题,我这么说肯定有人会对此表示怀疑,其实系统到底能撑多少IP/天不在于系统本身而是在于使用该系统的人...

    R的极客理想:工具篇,完整扫描版

    书中内容来自作者在R语言的实际使用过程中的经验总结,其中涉及计算机、互联网、数据库、大数据、统计、金融等领域,详细总结了R语言在实际使用时与Java、MySQL、Redis、MongoDB、Cassandra、Hadoop、Hive、HBase等...

    leetcode下载-JavaTopic:Java面试题总结

    redis缓存穿透、缓存雪崩,有没有在实际的工作中遇到过,如果解决缓存雪崩问题; redis的有几种集群方式; redis的基本数据类型(String、List、Hash、Set、ZSet)的使用场景? redis集群; redis支持事务吗?如果不...

    Eclipse开发分布式商城系统+完整视频代码及文档

    项目中遇到的问题.avi │ 13.quartz.avi │ aa.quartz框架.avi │ 淘淘商城第十二天笔记.docx │ ├─补充1:拔高课程(Redis3.0持久化、集群、MySQL5.6优化、Tomcat7优化) │ │ 打开必读.txt │ │ │ ├─课前...

    基于SpringBoot的包含前端和后端的博客系统源码+项目说明.zip

    - 记录一些在开发过程中遇到的一些问题,总结开发技巧 **接下来,会以需求和设计方式来阐述** # 效果图 - 首页展示 ![image-20210105234019598](pic/image-20210105234019598.png) - 文章编辑 ![image-...

    自动化测试面试题总结.docx

    1,工作中所遇到的错误 2,测试流程: 3,测试计划元素: 4,测试报告元素: 5,测试点: 6,测试方法: 7,bug相关问题 8,adb常用命令 9,软件测试原则 10,测试用例编写的要素 11,测试用例的设计原则 12,软件...

    java-component-practice:Java工程师成长计划-学习总结

    本项目是作者把自己平时学习,开发项目遇到的问题,和不熟悉的组件等相关练习记录下来。以演示练习+记录笔记的形式更好的学习Java以及Java相关的组件。同时也是为了以后开发过程中方便各个组件的整合,达到各个组件...

    详解一种用django_cache实现分布式锁的方式

    在项目开发过程中,我遇到一个需求:对于某条记录,一个用户对它进行操作时会持续比较久,希望在一个用户的操作期间,不允许有另一个用户操作它,否容易会出现混乱。 在与同事们讨论后,想通过加锁的方式,起初想用...

    PHP-Interview:分享和记录常见的php面试题和面试技巧

    你该知道的 7 个必经阶段为什么写这个常言道,好记性,不如烂笔头,在笔者几次面试的过程中,总会遇到一些常见的问题,特此记录下来,也是对自己学习知识的一个总结和梳理,构建一个面试体系,而不必慌张的临时准备...

    基于Java的技术交流网站的设计与实现.doc

    Web产品的功能也愈发复杂、对相关技术人员的要求也越来越高,而技术 人员在开发过程中难免会遇到各色棘手的问题,所以面向开发人员的技术交流网站应运 而生。由此决定使用Vue.js这个MVVM框架以及基于Vue.js的各种...

    java面试题,180多页,绝对良心制作,欢迎点评,涵盖各种知识点,排版优美,阅读舒心

    180多页面试题,前前后后不间断的更新了...【Dubbo】dubbo使用中遇到的问题 174 【Zookeeper】zookeeper介绍 178 1、简介 178 2、基本概念 179 3. ZooKeeper典型应用场景 183 4、ZooKeeper在大型分布式系统中的应用 189

Global site tag (gtag.js) - Google Analytics