1、redis-cluster集群
使用主从复制当master挂掉之后系统不能自动选举出新的master,系统也就不能对外提供写功能;使用哨兵模式虽然解决了自动选举的问题,但是不能动态扩展,下面介绍redis-cluster。
redis-cluster集群中有多个master节点,每个master都可读可写,节点之间相互通信,redis-cluster集群无中心节点,无需哨兵监控。
常见问题:
①、Redis是如何进行持久化的?
Redis是基于内存的,因此持久化是必不可少的,在Redis中持久化有两种方式,即:RDB和AOF
- RDB,redis会定期的生成RDB文件,这个文件数据为当前数据,这个时间一般默认5分钟甚至更长,因此在此期间可能会丢失数据,而且RDB方式会产生多个文件,每次生成RDB文件redis会fork一个子进程来完成,因此生成操作消耗很小,而且RDB文件相对于AOF文件来说要比AOF文件小的多,所以执行RDB文件恢复数据要比AOF文件快。另外,如果使用RDB方式在某一时间段数据量很大,RDB生成期间会导致客户端暂停几毫秒甚至几秒,在秒杀这一场景中是致命的。
- AOF,这种持久化的方式会把每次写数据的命令都保存在一个AOF文件中,此文件只支持append-only,也就是只支持添加,所以这个文件必然是很大的,然而此种方式并不是能保证数据,因为AOF方式会每一秒一次通过一个子线程fsync,可能会丢失这一秒的数据,如果RDB和AOF都配置了会优先使用AOF恢复。
因此针对上述两种方式的优缺点,一般选择二者都使用,比如先使用RDB恢复某一时间段的数据,然后在使用AOF方式补全。
②、主从之间的数据同步
先引用一张图
当有slave连接到master之后,会向master发出一个psync命令,如果slave是第一次连接master,会产生全量复制,master启动一个线程生成RDB文件,然后将在此期间的缓存所有的写命令,然后将RDB快照发给slave,随后将缓存的命令发送给slave。如果是因为网络中断等原因造成的中断会导致增量复制,中断后master将写命令缓存,slave连接后将标识自己的run_id和偏移量offset发送给master,如果offset在master缓冲范围内就继续发送,增量发送的关键在于偏移量offset。
③、Redis采用的过期策略
定期过期+惰性过期,定期过期就是定时一段时间随机检查部分数据是否过期,不扫描全部数据检查过期的原因很简单,就是因为Redis数据可能很多,全部扫描如同MySQL中查询全表扫描。
如果定期没删除,而且惰性过期也没查询到,此时就用到了内存淘汰策略,Redis有六种内存淘汰策略:
- noeviction,返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令
- allkeys-lru,尝试回收最少使用的键(LRU),使得新添加的数据有空间存放
- allkeys-random,回收随机的键使得新添加的数据有空间存放
- volatile-lru,尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放
- volatile-random,回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键
- volatile-ttl,回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。
如果没有键满足回收的前提条件的话,策略volatile-lru, volatile-random以及volatile-ttl就和noeviction 差不多了。
也就是说Redis的key失效策略是:定期过期+惰性过期+内存淘汰
④、LRU实现
public class LRU<K,V> extends LinkedHashMap<K,V> implements Map<K,V> {
public LRU(int initialCapacity, float loadFactor, boolean accessOrder) {
super(initialCapacity, loadFactor, accessOrder);
}
@Override
protected boolean removeEldestEntry(Entry<K, V> eldest) {
return size() > 6;
}
public static void main(String[] args) {
LRU<Character,Integer> lru = new LRU<>(16,.75f,true);
String s = "abcdefghdg";
for (int i = 0; i < s.length(); i++){
lru.put(s.charAt(i),i);
}
/**
* g=6, i=8, a=9, x=10, b=11, h=7
* g=6, i=8, x=10, b=11, a=12, h=7
* a b c d e f g h i j k l
*/
System.out.println("LRU中key为h的Entry的值为: " + lru.get('h'));
System.out.println("LRU的大小 :" + lru.size());
System.out.println("LRU :" + lru);
}
}
推荐阅读:
大厂都喜欢这样问Redis,哨兵、持久化、主从、手撕LRU,我都整理好了
2、相关问题
2.1、基于Redis的分布式锁
可以使用setnx命令获取锁,然后使用expire做过期,防止死锁,但是考虑到在setnx之后expire之前因为某些原因导致之前的进程需要重新维护这个锁就得不到释放,所以此时可以将setnx和expire两条命令合成一条指令来用。
2.2、如何从1亿数据中取出固定前缀的数据
有两种方式
- 使用keys命令,但因为redis是单线程的,keys返回结果前会被阻塞,如果当前redis是给线上使用,则线上服务会停顿直到keys返回结果。
- scan,scan可以无阻塞的取出目标数据,但会有重复值,因此需要手动去重,而且此命令需要的时间比keys命令时间长。
2.3、缓存雪崩
简单来说就是Redis缓存同一时间大量失效,导致请求全部走数据库,导致数据库瘫痪。
常见的解决方案是将每个key的过期时间都加一个随机时间,如
setRedis(Key,value,time + Math.random() * 10000);
2.4、缓存穿透
缓存穿透是指缓存和数据库中都不存在,导致所有的请求都到数据库,导致数据库瘫痪。比如用户ID置为大于0的,但是请求接口传来的是-1,解决方法有下面几种
- ①、在接口层增加校验,对参数和用户权限等进行校验,不符合要求直接返回。
- ②、对于在缓存中不存在,访问数据库时也不存在,可以将此key设置为一个缓存,并设置合适的过期时间,将null、不存在等字眼作为value,防止下次在请求直接走数据库。
- ③、为了防止id暴力攻击,可以在网管层做一些处理,比如一秒内IP设置最多访问次数,多于这个就将这个IP拉黑等。
- ④、布隆过滤器,关于布隆过滤器推荐阅读:Redis-避免缓存穿透的利器之BloomFilter
2.5、缓存击穿
缓存击穿是缓存中没有,但数据库中存在,它于缓存雪崩不一致的地方是缓存雪崩是大量key过期,可能是不同的请求,而缓存击穿是一个key过期,导致大量请求到达数据库,导致数据库瘫痪,解决方案有两种
- 第一种解决方案就是设置key永不过期;
- 第二种就是加上互斥锁