1. Redis常用数据结构
string、hash、list、set、sortedset
2. Redis数据持久化
2.1 RDB持久化
1.save 同步的(会很慢,如存在老数据会被替换)
2.bgsave 异步持久化(fork一个子进程,fork慢也会阻塞redis,但很少出现)
3.自动
如图:三个条件满足其一就会自动持久化RDB文件
1.900秒发生1次改变,自动生成保存RDB文件
2.300秒发生10次改变,自动生成保存RDB文件
3.60秒发生10000次改变,自动生成保存RDB文件
dbfilename:rdb文件存储名称,因为会存在一台机器多个redis实例,可用端口号区分
dir :文件存储位置
stop-writes... :出错时停止写入
rdbcompression : rdb文件校验
RDB的优点:
- 对性能影响最小。如前文所述,Redis在保存RDB快照时会fork出子进程进行,几乎不影响Redis处理客户端请求的效率。
- 每次快照会生成一个完整的数据快照文件,所以可以辅以其他手段保存多个时间点的快照(例如把每天0点的快照备份至其他存储媒介中),作为非常可靠的灾难恢复手段。
- 使用RDB文件进行数据恢复比使用AOF要快很多。
RDB的缺点:
- 快照是定期生成的,所以在Redis crash时或多或少会丢失一部分数据。
- 如果数据集非常大且CPU不够强(比如单核CPU),Redis在fork子进程时可能会消耗相对较长的时间(长至1秒),影响这期间的客户端请求。
2.2 AOF持久化
AOF提供了三种fsync配置,always/everysec/no,通过配置项[appendfsync]指定:
- appendfsync no:不进行fsync,将flush文件的时机交给OS决定,速度最快
- appendfsync always:每写入一条日志就进行一次fsync操作,数据安全性最高,但速度最慢
- appendfsync everysec:折中的做法,交由后台线程每秒fsync一次(通常采用)
AOF的优点:
- 最安全,在启用appendfsync always时,任何已写入的数据都不会丢失,使用在启用appendfsync everysec也至多只会丢失1秒的数据。
- AOF文件在发生断电等问题时也不会损坏,即使出现了某条日志只写入了一半的情况,也可以使用redis-check-aof工具轻松修复。
- AOF文件易读,可修改,在进行了某些错误的数据清除操作后,只要AOF文件没有rewrite,就可以把AOF文件备份出来,把错误的命令删除,然后恢复数据。
AOF的缺点:
- AOF文件通常比RDB文件更大
- 性能消耗比RDB高
- 数据恢复速度比RDB慢
3.数据淘汰机制
Redis提供了5种数据淘汰策略:
- volatile-lru:使用LRU算法进行数据淘汰(淘汰上次使用时间最早的,且使用次数最少的key),只淘汰设定了有效期的key
- allkeys-lru:使用LRU算法进行数据淘汰,所有的key都可以被淘汰
- volatile-random:随机淘汰数据,只淘汰设定了有效期的key
- allkeys-random:随机淘汰数据,所有的key都可以被淘汰
- volatile-ttl:淘汰剩余有效期最短的key
一般来说,推荐使用的策略是volatile-lru,并辨识Redis中保存的数据的重要性。对于那些重要的,绝对不能丢弃的数据(如配置类数据等),应不设置有效期,这样Redis就永远不会淘汰这些数据。对于那些相对不是那么重要的,并且能够热加载的数据(比如缓存最近登录的用户信息,当在Redis中找不到时,程序会去DB中读取),可以设置上有效期,这样在内存不够时Redis就会淘汰这部分数据。
配置方法:
maxmemory-policy volatile-lru #默认是noeviction,即不进行数据淘汰
4.基于Redis分布式锁
加锁代码
public class RedisTool {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
/**
* 尝试获取分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 超期时间
* @return 是否获取成功
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
可以看到,我们加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time)
,这个set()方法一共有五个形参:
- 第一个为
key
,我们使用key来当锁,因为key是唯一的。 - 第二个为value,我们传的是
requestId
,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。 - 第三个为nxxx,这个参数我们填的是
NX
,意思是SET IF NOT EXIST
,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作; - 第四个为expx,这个参数我们传的是
PX
,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。 - 第五个为
time
,与第四个参数相呼应,代表key的过期时间。
解锁代码
public class RedisTool {
private static final Long RELEASE_SUCCESS = 1L;
/**
* 释放分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
第一行代码,我们写了一个简单的Lua脚本代码。首先获取锁对应的value值,检查是否与requestId
相等,如果相等则删除锁(解锁)。那么为什么要使用Lua语言来实现呢?因为要确保上述操作是原子性的。
第二行代码,我们将Lua代码传到jedis.eval()
方法里,并使参数KEYS[1]
赋值为lockKey
,ARGV[1]
赋值为requestId
。eval()
方法是将Lua代码交给Redis服务端执行。
5.缓存使用常见问题
1. 为啥redis单线程模型也能效率这么高?
- 纯内存操作
- 核心是基于非阻塞的IO多路复用机制
- 单线程反而避免了多线程的频繁上下文切换问题
2. 缓存雪崩现象及如何解决缓存雪崩
- 缓存雪崩现象
高并发时缓存挂掉,所有请求打到数据库 - 处理缓存雪崩
事前:redis高可用,主从+哨兵,redis cluster,避免全盘崩溃
事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL被打死
事后:redis持久化,快速恢复缓存数据
3. 缓存穿透问题
发来许多缓存没有数据的请求,导致直接查数据库
解决方案:查缓存没查到时,设置一个标志返回,下次走缓存
4. 缓存数据库双写一致问题
最经典的缓存+数据库读写的模式 Cache Aside Pattern
(1)读的时候,先读缓存,缓存没有的话,那么就读数据库,然后取出数据后放入缓存,同时返回响应
(2)更新的时候,先删除缓存,然后再更新数据库
- 最初级的数据库+缓存双写不一致问题
问题:先修改数据库,再删除缓存,如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据出现不一致
解决思路:先删除缓存,再修改数据库,如果删除缓存成功了,如果修改数据库失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致
因为读的时候缓存没有,则读数据库中旧数据,然后更新到缓存中
- 上亿流量高并发场景下数据不一致问题
方案:数据库与缓存更新与读取操作进行异步串行化
更新数据的时候,根据数据的唯一标识,将操作路由之后,发送到一个jvm内部的队列中。
读取数据的时候,如果发现数据不在缓存中,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也发送同一个jvm内部的队列中。
一个队列对应一个工作线程,每个工作线程串行拿到对应的操作,然后一条一条的执行。这样的话,一个数据变更的操作,先执行,删除缓存,然后再去更新数据库,但是还没完成更新。此时如果一个读请求过来,读到了空的缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成。
这里有一个优化点,一个队列中,其实多个更新缓存请求串在一起是没意义的,因此可以做过滤,如果发现队列中已经有一个更新缓存的请求了,那么就不用再放个更新请求操作进去了,直接等待前面的更新操作请求完成即可。
待那个队列对应的工作线程完成了上一个操作的数据库的修改之后,才会去执行下一个操作,也就是缓存更新的操作,此时会从数据库中读取最新的值,然后写入缓存中。如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回; 如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值