1、问题及背景 redis
公司去年上线一个抽奖系统,主要用来拉新、提高流量,全部新注册的用户在指定时间均可以抽奖,为了保证安全性,程序中作了频率限制,每一个用户30秒只能抽1次,具体作法是以用户id为key,保存在redis中,过时时间为30秒;抽奖时会先读取这个key是否存在,若是存在则认为用户在30秒内已经抽过,返回稍后再试。sql
由于读多写少,为了提升系统的吞吐量,系统采用了redis读、写分离的架构,即写入的时候往master上写,读取用户是否抽过奖则从slave上读取,redis版本为2.8.6。安全
这个系统上线后前几天运行比较良好,某天忽然报大量的稍后重试的错误,很多用户反馈抽了一次奖后再也没法抽奖。架构
经过日志分析和数据核对发现某个key过时了,但在slave上还能够读取的到。函数
复现以下:oop
在master上设置一个key,并设置过时时间this
set name edward expire name 5
过了5秒等key过时后再到slave上读取,但get返回不为空spa
但这个时候若是在master上get1次,再到slave上get,结果就是空了。设计
2、故障分析3d
为何会出现这种状况呢,咱们来分析下redis中key过时删除策略,redis中key过时删除策略有二种:主动删除、惰性删除。
一、主动删除
是在服务端的定时任务中执行,相关代码以下:
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { ... databasesCron(); ...
serverCron函数在redis启动的时候注册到定时器中,执行频率大概为100毫秒1次,具体参考aeCreateTimeEvent函数。
其中databasesCron函数为将过时的key进行随机删除:
if (server.active_expire_enabled && server.masterhost == NULL) activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);
这里会判断当前实例是否为主实例,只有主实例而且active_expire_enabled 启用(默认会启用)才会启动删除机制,activeExpireCycle函数中会随机删除一些过时的key,注意是随机删除一些过时的key,而不是所有删除,由于redis要考虑系统的负载,怕执行时间太长会抢占太多CPU,增长系统负载,这里就不细讲,有兴趣的同窗能够细看下代码。
结论:主动删除只有在master上生效。
二、惰性删除
再看get命令:
int getGenericCommand(redisClient *c) { robj *o; if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)) == NULL) return REDIS_OK; if (o->type != REDIS_STRING) { addReply(c,shared.wrongtypeerr); return REDIS_ERR; } else { addReplyBulk(c,o); return REDIS_OK; } }
lookupKeyReadOrReply函数会调用lookupKeyRead函数,后者会调用到expireIfNeeded先检测是否过时了:
int expireIfNeeded(redisDb *db, robj *key) { mstime_t when = getExpire(db,key); mstime_t now; /* If we are running in the context of a slave, return ASAP: * the slave key expiration is controlled by the master that will * send us synthesized DEL operations for expired keys. * * Still we try to return the right information to the caller, * that is, 0 if we think the key should be still valid, 1 if * we think the key is expired at this time. */ if (server.masterhost != NULL) return now > when; }
经过代码发现,若是当前实例为slave则直接返回,不会作进一步的处理;做者也作了注释,说slave上过时的key会依赖master发过来的DEL命令来删除。
结论:redis的惰性删除机制是在执行用户请求的时候判断key是否过时,惰性删除也只在master上生效,slave上是不生效的。
咱们来总结下:
一、redis中过时key的删除有2种策略:主动删除、惰性删除。
二、主动删除和惰性删除只在master上发生,slave的删除机制依赖于master。
回到上面的问题,是什么缘由致使key过时了,而slave上还有值,由于master没有及时将过时的key删除,即没有触发主动删除机制,这时候也没有在master上读取数据,即执行get命令,因此也不会触发master上的惰性删除机制,因此slave上的key没有及时删除。
为何master的主动删除没有触发呢?,缘由有二:
一、redis的定时任务执行有延迟
redis尽可能保证按指定时间执行指定任务,不过若是当时CPU抢占的比较厉害,定时任务执行时间可能有很大的延迟,这个期间一些key没有及时删除。
这种状况通常发生在redis实例所在机器cpu负载很高的状况。
二、由于redis的是随机删除的,可能会致使部分过时key没有被及时删除掉
这个只发生在redis中有大量的过时的key的状况下
3、解决方案
好了,问题缘由找到了,那咱们的解决方案是什么呢?
禁止在slave上查询一些关键信息:像锁、登陆信息,这些信息必须从master上查询,若是压力较大能够经过集群方案多堆些机器。