1. 过期时间
除了string数据类型有setex可以在设置数据同时设置过期时间外,其余的数据类型都需要依靠expire命令设置过期时间,这个命令是针对key的。
1.1 过期时间有什么用?
- redis的数据保存到内存中,如果每个数据都永久保存,很快就OOM了。
- 如果有一个业务场景只需要某个数据在某段时间存在,例如短信验证码在120s内有效,用户登录的token在一天内有效,这种情况,如果把数据存到MySQL,我们需要再去写个方法判断过期,移除数据,这样非常麻烦,而且性能要差很多。这种情况下redis的过期时间就帮上大忙了。
1.2 如何判断数据过期
redis通过一个过期字典(可以看做hash表)保存数据过期的时间。过期字典的键指向redis中某个key,过期字典的值是保存了键所指向的redis的key的过期时间(毫秒精度)
其在C语言中的代码是这样的
typedef struct redisDb {
...
dict *dict; // 数据库键空间,保存着数据库中所有键值对
dict *expires // 过期字典,保存着键的过期时间
...
} redisDb;
1.3 过期数据如何删除?
当redis中的数据过期了,redis的过期策略是什么?
过期策略通常有以下三种:
-
定时过期
每个设置过期时间的key都要创建一个定时器,过期时间一到,数据立即清除。
这种策略对内存很友好,但是会占用大量CPU资源处理过期数据,影响响应时间和吞吐量。
这种策略redis并没有采用。 -
惰性过期
只有当用户访问数据时,才会判断这个数据是否已经过期,若过期则清除数据。
这种策略可以节省CPU资源,但是对内存不友好,可能出现大量过期数据未被访问,占用大量内存的情况。 -
定期清除
每隔一段时间,扫描过期字典中一定数量的数据,并清除其中过期的数据。
该方案可以在内存和CPU中达到一种平衡。
redis底层也会通过限制删除操作执行的时长和频率减少删除操作对CPU的影响。
redis采用惰性+定期清除的过期策略。
1.4 redis内存淘汰机制
上面采用的两种过期策略还是有问题的,因为还是可能出现漏掉移除大量过期数据的情况,容易导致OOM。
以及,我们如何保证redis中的数据都是热点数据?
这时候我们就要通过内存淘汰机制。
Redis 提供 6 种数据淘汰策略:
- volatile-lru(least recently used)
从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰 - volatile-ttl
从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰 - volatile-random
从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰 - allkeys-lru(least recently used)
当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的) - allkeys-random
从数据集(server.db[i].dict)中任意选择数据淘汰 - no-eviction:
禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。
4.0 版本后增加以下两种
- volatile-lfu(least frequently used)
从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰 - allkeys-lfu(least frequently used)
当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key# Redis 持久化机制
2. 数据一致性
新增、更改、删除数据库操作同步更新redis,可以用事务机制保证数据一致性。
对于MySQL和redis的同步更新,一般有如下四种方案:
- 先更新数据库,后更新缓存
- 先更新缓存,后更新数据库
- 先删除缓存,后更新数据库
- 先更新数据库,后删除缓存
一般来说,第一种和第二种方案,不会被使用。
第一种方案的问题:在并发更新数据库场景下,若更新数据库与更新缓存数据顺序不一致,会导致缓存中是脏数据。若最后一条数据库更新操作成功,缓存更新失败,也会导致缓存中是脏数据。
第二种方案的问题:如果缓存更新成功,而数据库更新失败,会造成数据库缓存数据不一致问题。
三、四两种方案都是不保留缓存的,只能等下次读取数据库操作再把数据更新到缓存上。
2.1 先删除缓存,后更新数据库
这种情况也可能导致脏数据的出现。
有两个请求:A(更新操作)、B(查询操作)
- A删除缓存,进行更新数据库操作。
- B查询缓存发现数据不存在,查询数据库拿到旧数据。
- B把旧数据更新到缓存。
- A把新数据写入数据库。
这造成了数据库缓存数据不一致的情况,我们通过以下两种方式解决:
2.1.1 延时双删
用伪代码表示
public void write(String key,Object data){
redis.delKey(key);
database.updateData(data);
Thread.sleep(1000);
redis.delKey(key);
}
也就是在数据库更新数据完成后,休眠一段时间再次删除缓存,这样可以避免在更新数据库期间有读操作把脏数据更新到缓存中。
2.1.2 更新与读取操作异步串行
一个数据变更的操作,先执行删除缓存,然后再去更新数据库,但是还没完成更新的时候,如果此时一个读请求过来,读到了空的缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,排在刚才更新库的操作之后,然后同步等待缓存更新完成,再读库。如果这时有多个读该数据的请求,可以做去重处理。
2.2 先更新数据库,后删除缓存
这种情况下可能发生的问题是:数据库更新操作成功了,但是缓存删除操作失败了,导致缓存出现脏数据。
解决方案就是利用消息队列进行删除的补偿
- 请求A先对数据库进行更新操作。
- 在对缓存进行删除操作时发现删除失败。
- 此时将缓存的key作为消息体发送到消息队列。
- 系统接收到消息队列发送的消息,再次删除缓存。