海量数据和高并发下的Redis业务优

转自:码洞ID:codehole

观众朋友们,我是来自掌阅的工程师钱文品,今天我带来的是分享主题是:Redis在海量数据和高并发下的优化实践。Redis对于从事互联网技术工程师来说并不陌生,几乎所有的大中型企业都在使用Redis作为缓存数据库,但是对于绝大多数企业来说只会用到它的最基础的KV缓存功能,还有很多Redis的高级功能可能都未曾认真实践过。今天在这一个小时的时间我会围绕Redis,分享在平时的日常业务开发中遇到的9个经典案例,希望通过此次分享可以帮助大家更好的将Redis的高级特性应用到日常的业务开发中来。

掌阅电子书阅读软件ireader的总用户量大概是5亿左右,月活5kw,日活近2kw。服务端有多个Redis实例,+集群,每个实例的内存控制在20g以下。

KV缓存

第一个是最基础也是最常用的就是KV功能,我们可以用Redis来缓存用户信息、会话信息、商品信息等等。下面这段代码就是通用的缓存读取逻辑。

defget_user(user_id):user=redis.get(user_id)ifnotuser:user=db.get(user_id)redis.setex(user_id,ttl,user)//设置缓存过期时间returnuserdefsave_user(user):redis.setex(user.id,ttl,user)//设置缓存过期时间db.save_async(user)//异步写数据库

这个过期时间非常重要,它通常会和用户的单次会话长度成正比,保证用户在单次会话内尽量一直可以使用缓存里面的数据。当然如果贵公司财力雄厚,又极致注重性能体验,可以将时间设置的长点甚至干脆就不设置过期时间。当数据量不断增长时,就使用Codis或者Redis-Cluster集群来扩容。

除此之外Redis还提供了缓存模式,Set指令不必设置过期时间,它也可以将这些键值对按照一定的策略进行淘汰。打开缓存模式的指令是:configsetmaxmemory20gb,这样当内存达到20gb时,Redis就会开始执行淘汰策略,给新来的键值对腾出空间。这个策略Redis也是提供了很多种,总结起来这个策略分为两块:划定淘汰范围,选择淘汰算法。比如我们线上使用的策略是allkeys-lru。这个allkeys表示对Redis内部所有的key都有可能被淘汰,不管它有没有带过期时间,而volatile只淘汰带过期时间的。Redis的淘汰功能就好比企业遇到经济寒冬时需要勒紧裤腰带过冬需要进行一轮残酷的人才优化。它会选择只优化临时工呢,还是所有人一律平等都可能被优化。当这个范围圈定之后,会从中选出若干个名额,怎么选择呢,这个就是淘汰算法。最常用的就是LRU算法,它有一个弱点,那就是表面功夫做得好的人可以逃过优化。比如你乘机赶紧在老板面前好好表现一下,然后你就安全了。所以到了Redis4.0里面引入了LFU算法,要对平时的成绩也进行考核,只做表面功夫就已经不够用了,还要看你平时勤不勤快。最后还一种极不常用的算法——随机摇号算法,这个算法有可能会把CEO也给淘汰了,所以一般不会使用它。

分布式锁

下面我们看第二个功能——分布式锁,这个是除了KV缓存之外最为常用的另一个特色功能。比如一个很能干的资深工程师,开发效率很快,代码质量也很高,是团队里的明星。所以呢诸多产品经理都要来烦他,让他给自己做需求。如果同一时间来了一堆产品经理都找他,它的思路呢就会陷入混乱,再优秀的程序员,大脑的并发能力也好不到哪里去。所以呢他就在自己的办公室的门把上挂了一个请勿打扰的牌子,当一个产品经理来的时候先看看门把上有没有这个牌子,如果没有呢就可以进来找工程师谈需求,谈之前要把牌子挂起来,谈完了再把牌子摘了。这样其它产品经理也要来烦他的时候,如果看见这个牌子挂在那里,就可以选择睡觉等待或者是先去忙别的事。如是这位明星工程师从此获得了安宁。

图片

这个分布式锁的使用方式非常简单,就是使用Set指令的扩展参数如下

#加锁setlock:$user_idowner_idnxex=5#释放锁ifredis.call("get",KEYS[1])==ARGV[1]thenreturnredis.call("del",KEYS[1])elsereturn0end#等价于del_if_equalslock:$user_idowner_id

一定要设置这个过期时间,因为遇到特殊情况——比如地震(进程被kill-9,或者机器宕机),产品经理可能会选择从窗户上跳下去,没机会摘牌,导致了死锁饥饿,让这位优秀的工程师成了一位大闲人,造成严重的资源浪费。同时还需要注意这个owner_id,它代表锁是谁加的——产品经理的工号。以免你的锁不小心被别人摘掉了。释放锁时要匹配这个owner_id,匹配成功了才能释放锁。这个owner_id通常是一个随机数,存放在ThreadLocal变量里(栈变量)。

官方其实并不推荐这种方式,因为它在集群模式下会产生锁丢失的问题——在主从发生切换的时候。官方推荐的分布式锁叫RedLock,作者认为这个算法较为安全,推荐我们使用。不过掌阅这边一直还使用上面最简单的分布式锁,为什么我们不去使用RedLock呢,因为它的运维成本会高一些,需要3台以上独立的Redis实例,用起来要繁琐一些。另外呢Redis集群发生主从切换的概率也并不高,即使发生了主从切换出现锁丢失的概率也很低,因为主从切换往往都有一个过程,这个过程的时间通常会超过锁的过期时间,也就不会发生锁的异常丢失。还有呢就是分布式锁遇到锁冲突的机会也不多,这正如一个公司里明星程序员也比较有限一样,总是遇到锁排队那说明结构上需要优化。

延时队列

下面我们继续看第三个功能——延时队列。前面我们提到产品经理在遇到「请勿打扰」的牌子时可以选择多种策略,1.干等待2.睡觉2.放弃不干了3.歇一会再干。干等待就是spinlock,这会烧CPU,飙高Redis的QPS。睡觉就是先sleep一会再试,这会浪费线程资源,还会增加响应时长。放弃不干呢就是告知前端用户待会再试,现在系统压力大有点忙,影响用户体验。最后一种呢就是现在要讲的策略——待会再来,这是在现实世界里最普遍的策略。这种策略一般用在消息队列的消费中,这个时候遇到锁冲突该怎么办?不能抛弃不处理,也不适合立即重试(spinlock),这时就可以将消息扔进延时队列,过一会再处理。

图片

有很多专业的消息中间件支持延时消息功能,比如RabbitMQ和NSQ。Redis也可以,我们可以使用zset来实现这个延时队列。zset里面存储的是value/score键值对,我们将value存储为序列化的任务消息,score存储为下一次任务消息运行的时间(deadline),然后轮询zset中score值大于now的任务消息进行处理。

#生产延时消息zadd(queue-key,now_ts+5,task_json)#消费延时消息whileTrue:task_json=zrevrangebyscore(queue-key,now_ts,0,0,1)iftask_json:grabbed_ok=zrem(queue-key,task_json)ifgrabbed_ok:process_task(task_json)else:sleep()//歇1s

当消费者是多线程或者多进程的时候,这里会存在竞争浪费问题。当前线程明明将task_json从zset中轮询出来了,但是通过zrem来争抢时却抢不到手。这时就可以使用LUA脚本来解决这个问题,将轮询和争抢操作原子化,这样就可以避免竞争浪费。

localres=nillocaltasks=redis.pcall("zrevrangebyscore",KEYS[1],ARGV[1],0,"LIMIT",0,1)if#tasks0thenlocalok=redis.pcall("zrem",KEYS[1],tasks[1])ifok0thenres=tasks[1]endend

returnres

为什么我要将分布式锁和延时队列一起讲呢,因为很早的时候线上出了一次故障。故障发生时线上的某个Redis队列长度爆表了,导致很多异步任务得不到执行,业务数据出现了问题。后来查清楚原因了,就是因为分布式锁没有用好导致了死锁,而且遇到加锁失败时就sleep无限重试结果就导致了异步任务彻底进入了睡眠状态不能处理任务。那这个分布式锁当时是怎么用的呢?用的就是setnx+expire,结果在服务升级的时候停止进程直接就导致了个别请求执行了setnx,但是expire没有得到执行,于是就带来了个别用户的死锁。但是后台呢又有一个异步任务处理,也需要对用户加锁,加锁失败就会无限sleep重试,那么一旦撞上了前面的死锁用户,这个异步线程就彻底熄火了。因为这次事故我们才有了今天的正确的分布式锁形式以及延时队列的发明,还有就是优雅停机,因为如果存在优雅停机的逻辑,那么服务升级就不会导致请求只执行了一半就被打断了,除非是进程被kill-9或者是宕机。

定时任务

分布式定时任务有多种实现方式,最常见的一种是master-workers模型。master负责管理时间,到点了就将任务消息仍到消息中间件里,然后worker们负责监听这些消息队列来消费消息。著名的Python定时任务框架Celery就是这么干的。但是Celery有一个问题,那就是master是单点的,如果这个master挂了,整个定时任务系统就停止工作了。

图片

另一种实现方式是multi-master模型。这个模型什么意思呢,就类似于Java里面的Quartz框架,采用数据库锁来控制任务并发。会有多个进程,每个进程都会管理时间,时间到了就使用数据库锁来争抢任务执行权,抢到的进程就获得了任务执行的机会,然后就开始执行任务,这样就解决了master的单点问题。这种模型有一个缺点,那就是会造成竞争浪费问题,不过通常大多数业务系统的定时任务并没有那么多,所以这种竞争浪费并不严重。还有一个问题它依赖于分布式机器时间的一致性,如果多个机器上时间不一致就会造成任务被多次执行,这可以通过增加数据库锁的时间来缓解。

图片

现在有了Redis分布式锁,那么我们就可以在Redis之上实现一个简单的定时任务框架。

#注册定时任务hsettasksnametrigger_rule#获取定时任务列表hgetalltasks#争抢任务setlock:${name}truenxex=5#任务列表变更(滚动升级)#轮询版本号,有变化就重加载任务列表,重新调度时间有变化的任务settasks_version$new_versiongettasks_version

如果你觉得Quartz内部的代码复杂的让人看不懂,分布式文档又几乎没有,很难折腾,可以试试Redis,使用它会让你少掉点头发。

LifeisShort,IuseRedis







































北京哪个医院是白癜风医院
白癜风检查有哪些



转载请注明:http://www.92nongye.com/hxjs/204621723.html

  • 上一篇文章:
  •   
  • 下一篇文章: 没有了