大家在使用redis的时候,经常会用expire来设置key的过期时间,以为某个key到期就会马上清除。但在设置为主写随机读时,发现存在key未失效的情况,下面具体分析:
原因一过期策略的问题:
3.2之后的版本已不存在以下问题
Redis key的三种过期策略
惰性删除:当读/写一个已经过期的key时,会触发惰性删除策略,直接删除掉这个过期key,很明显,这是被动的!
定期删除:由于惰性删除策略无法保证冷数据被及时删掉,所以 redis 会定期主动淘汰一批已过期的key。(在第二节中会具体说明)
主动删除:当前已用内存超过maxMemory限定时,触发主动清理策略。主动设置的前提是设置了maxMemory的值
Redis删除过期Key的源码
<pre name="code" class="java">int expireIfNeeded(redisDb *db, robj *key) {
time_t when = getExpire(db,key);
if (when < 0) return 0; /* No expire for this key */
/* Don't expire anything while loading. It will be done later. */
if (server.loading) return 0;
/* 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 time(NULL) > when;
}
/* Return when this key has not expired */
if (time(NULL) <= when) return 0;
/* Delete the key */
server.stat_expiredkeys++;
propagateExpire(db,key);
return dbDelete(db,key);
}
原因二快照模式自身的特性 :
大家都知道,redis支持两 种持久化方式,一种是 Snapshotting(快照)也是默认方式,另一种是 Append-only file(缩写 aof)的方式。 Snapshotting(快照)的持久化方式:
这种方式是就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为 dump.rdb。可以通过配置设置自动做快照持久 化的方式。我们可以配置 redis 在 n 秒内如果超过 m 个 key 被修改就自动做快照,下面是默认的快照保存配置。
save 900 1 #900 秒内如果超过 1 个 key 被修改,则发起快照保存
save 300 10 #300 秒内容如超过 10 个 key 被修改,则发起快照保存
Save 60 10000 #60 秒内容如超过 10000个 key 被修改,则发起快照保存
下面介绍详细的快照保存过程
1.redis 调用 fork,现在有了子进程和父进程。
2. 父进程继续处理 client 请求,子进程负责将内存内容写入到临时文件。由于 os 的写时复制机制(copy on write)父子进程会共享相同的物理页面,当父进程处理写请求时 os 会为父进程要修改的页面创建副本,而不是写共享的页面。所以子进程的地址空间内的数 据是 fork 时刻整个数据库的一个快照。
3.当子进程将快照写入临时文件完毕后,用临时文件替换原来的快照文件,然后子进程退出。
由于快照模式是定时进行,可能会导致数据在上次持久化完成到本次持久化开始之前,redis服务器宕机,丢失内存中相关数据,这也是主从读取内容不一致的原因之一
二、 Redis 获取字段为null的问题解析
不知道大家是否碰到过一个问题,使用redis的hmget()获取列表形式的数据时,有时偶然会发生获取的列表中,所有字段都是null的情况。本人项目中碰到过,下面解释下部门大牛的调查结果:当存储的数据时存在有效期时,且数据在过期的时间临界点,此时,过期数据会被清理,关键是清理过程,是先删除List里每个元素的数据,再删除整个List的,这就导致小概率事件的发生了,hmget时,结果是List<String>中里面的元素都为null,也许这也是很多项目都使用hgetAll()的原因了。
所以我们可以在hmget增加一个校验处理:
// 使用hexists的地方必须对查询结果进行校验
if (CollectionUtils.isEmpty(result)|| isAllItemNull(result)){
return new ArrayList<String>();
}
这可以算是hmget()的一个坑了,但同样,
hgetAll()也存在一个坑的,只是一般不会发生,详细参见:http://huoding.com/2013/01/21/214