1、什么是Redis
Redis 是一个基于内存的高性能 key-value数据库。支持多种数据类型
2、简单描述Redis的特点
Redis本质上是一个key-value类型的内存数据库,很像memcached,整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据(内存中)flush到硬盘上进行保存。
纯内存操作,Redis的性能非常出色,每秒可以处理超过10万次读写操作,是已知性能最快的key-value DB
Redis的出色之处,不仅仅是性能,Redis最大的魅力是支持保存多种数据结构;
此外,当个value的最大限制是1GB,不像memcached只能保存1MB的数据,因此Redis可以用来实现很多有用的功能;
Redis的主要缺点,就是数据库容量受到物理内容的限制,不能用做海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上
3、Redis支持的数据类型
String 、List 、 Set、 Sorted Set、 Hash
4、为什么Redis需要把所有数据放到内存中
- 追求最快的数据读取速度,如果直接磁盘读取会非常慢
- 为了保证数据安全,也会异步方式将数据写入磁盘
- 可以设置Redis最大使用的内存,若达到内存限制后将不能继续存取数据
5、Redis是单线程的么?Redis为什么这么快,尤其是其采用单线程??
单线程
Redis是单线程处理网络指令请求,所以不需要考虑并发安全问题。
所有的网络请求都是一个线程处理,但不代表所有模块都是单线程。
高性能
因为它的所有的数据都在内存中,所有的运算都是内存级别的运算,而且单线程避免了多线程的切换性能损耗问题;
而且正因为Redis是单线程,所以要小心使用Redis指令,对于那些耗时的指令(比如keys),一定要谨慎使用,一步小心就可能会导致Redis卡顿;
Redis单线程处理多个并发客户端连接:IO多路复用
Redis的IO多路复用:redis利用epoll来实现IO多路复用,将连接信息和事件放到队列中,依次放到文件事件分派器,事件分派器将事件分发给事件处理器。
Nginx也是采用IO多路复用原理解决C10k问题
6、Redis的持久化机制有哪些?区别是什么?优缺点是什么?
1.RDB持久化:原理是将Redis在内存中的数据库记录定时dump到磁盘上的RDB持久化
2.AOF(append only file)持久化:原理是将Redis的操作日志以追加的方式写入文件
区别:
RDB持久化的指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储
AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录
RDB 优点:
- RDB是紧凑的二进制文件,比较合适备份,全量复制等场景
- RDB恢复数据远快于AOF
RDB缺点:
- RDB无法实现实时或者秒级持久化
- 新老版本无法兼容RDB格式
AOF优点:
- 可以更好地保护数据不丢失
- appen-only模式读取性能比较高
- 适合做灾难性的误删除紧急恢复
AOF缺点:
- 对于同一份文件,AOF文件要比RDB快照大
- AOF开启后,写的QPS会有所影响,相对于RDB来说,写QPS要下降
- 数据库恢复比较慢,不适合做冷备
7、Redis的缓存失效策略有哪几种
- 定时删除策略
在设置key的过期时间的同时,为该key创建一个定时器,让定时器在可以的过期时间来临时,对可以进行删除
优点:保证内存尽快释放
缺点:如果key过多,删除这些key会占用很多CPU时间,而且每个key创建一个定时器,验证影响性能
-
惰性删除策略
key过期的时候不删除,每次从数据库获取key的时候去检查是否过期,若过期,则删除,返回null优点:CPU占用时间比较少
缺点:如果key很长时间没有被获取,将不会被删除,可能造成内存泄漏 -
定期删除策略
每隔一段时间执行一次删除(在redis.conf配置文件设置hz,1s刷新的频率)过期key的操作优点:可以控制删除操作的时长和频率,来减少CPU时间占用,可以避免惰性删除时候内存泄漏的问题
缺点:对内存友好方面,不如定时策略;对cpu友好方面,不如惰性策略
Redis一般采用惰性策略+定期策略两个相结合
8、redis的同步删除和异步删除
同步删除 : 删除key时释放value空间是在主线程中执行。
异步删除 : 删除key时释放value空间是在异步线程中执行。
Redis服务自身对Key的删除,可以分为「同步删除」和「异步删除」。使用DEL命令会触发「同步删除」,如果Key是一个有很多元素的复杂类型,这个过程可能会堵塞一下Redis服务自身,从而影响用户的访问,所以对于bigkey不能直接del删除。
同步和异步的区别就是释放value空间是主线程去执行还是异步线程去执行,理解这句话很关键。其他的操作都是由主线程执行的。
4.0以上的版本,默认是开启异步删除的,即lazyfree-lazy-expire=yes。
redis在进行key的过期删除的时候,如果开启了异步删除,则当被删除的key所对应的val占用空间大于64字节时,会将这个key标记为删除后直接返回+OK,然后将val放到后台的bio线程里面进行删除,防止阻塞主线程;
如果占用的空间小于64字节,即使开启了异步删除,在最后运行的时候也会同步的进行删除(redis优秀的性能优化在细节之末随处可见,针对很多场景都做了优化,并抽象出参数给用户动态配置,它的高性能是与redis作者精益求精的修改分不开的)。
8、什么是缓存命中率?提高缓存命中率的方法有哪些??d
- 命中:可以直接通过缓存获取到需要的数据
- 不命中:无法直接通过缓存获取到想要的数据,需要再次查询数据库或者执行其他的操作,原因可能是由于缓存中根本不存在,或者缓存已经过期
命中率越高表示使用缓存作用越好,性能越高(响应时间越短,吞吐量越高),并发能力也越好。
重点关注访问评率高且时效性相对低一些的业务数据上,利用预加载(预热)、扩容、优化缓存丽都。更新缓存等手段来提高命中率
9、redis分布式锁原理
9.1 单机模式
Redisson底层原理简单描述:
先判断一个key存在不存在,如果不存在,则set key,同时设置过期时间和value(1),
这个过程使用lua脚本来实现,可以保证多个命令的原子性,当业务完成以后,删除key;
如果存在说明已经有别的线程获取锁了,那么就循环等待一段时间后再去获取锁
如果是可重入锁呢:
先判断一个key存在不存在,如果不存在,则set key,同时设置过期时间和value(线程id:1),
如果存在,则判断value中的线程id是否是当前线程的id,如果是,说明是可重入锁,则value+1,变成(线程id:2),如果不是,说明是别的线程来获取锁,则获取失败;这个过程同样使用lua脚本一次性提交,保证原子性。
如何防止业务还没执行完,但是锁key过期呢,可以在线程加锁成功后,启动一个后台进程看门狗,去定时检查,如果线程还持有锁,就延长key的生存时间——Redisson就是这样实现的。
其实Jedis也有现成的实现方式,单机、集群、分片都有实现,底层原理是利用连用setnx、setex指令
(Redis从2.6之后支持setnx、setex连用)
jedis.set(key, value, "NX", "PX", expire)
注:setnx和setex都是原子性的
SETNX key value:
将 key 的值设为 value ,当且仅当 key 不存在;若给定的 key 已经存在,则 SETNX 不做任何动作。
相当于是 EXISTS 、SET 两个命令连用
SETEX key seconds value:
将value关联到key, 并将key的生存时间设为seconds(以秒为单位);如果key 已经存在,SETEX将重写旧值;
相当于是SET、EXPIRE两个命令连用
9.2 Cluster集群模式
很明显,上面介绍的分布式锁的实现只支持单机redis,工作中我们最常用的还是Cluster集群模式,上面的实现方式在集群模式下,是存在问题的,Cluster集群模式介绍见Redis(四):集群模式
整个过程如下:
- 客户端1在Redis的节点A上拿到了锁;
- 节点A宕机后,客户端2发起获取锁key的请求,这时请求就会落在节点B上;
- 节点B由于之前并没有存储锁key,所以客户端2也可以成功获取锁,即客户端1和客户端2同时持有了同一个资源的锁。
针对这个问题。Redis作者antirez提出了RedLock算法来解决这个问题
9.2.1 RedLock算法
RedLock算法思路如下:
获取当前时间的毫秒数startTime;
按顺序依次向N个Redis节点执行获取锁的操作,这个获取锁的操作和前面单Redis节点获取锁的过程相同,同时锁超时时间应该远小于锁的过期时间;
如果客户端向某个Redis节点获取锁失败/超时后,应立即尝试下一个Redis节点;
失败包括Redis节点不可用或者该Redis节点上的锁已经被其他客户端持有如果客户端成功获取到超过半数的锁时,记录当前时间endTime,同时计算整个获取锁过程的总耗时costTime = endTime - startTime,如果获取锁总共消耗的时间远小于锁的过期时间(即costTime < expireTime),则认为客户端获取锁成功,否则,认为获取锁失败
如果获取锁成功,需要重新计算锁的过期时间。它等于最初锁的有效时间减去第三步计算出来获取锁消耗的时间,即expireTime - costTime
如果最终获取锁失败,那么客户端立即向所有Redis发起释放锁的操作。(和单机释放锁的逻辑一样)
9.2.2 缺陷
RedLock算法虽然可以解决单点Redis分布式锁的安全性问题,但如果集群中有节点发生崩溃重启,还是会对锁的安全性有影响的。
假设一共有5个Redis节点:A, B, C, D, E。设想发生了如下的事件序列:
- 客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住);
- 节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了;
- 节点C重启后,客户端2锁住了C, D, E,获取锁成功;
这样,客户端1和客户端2同时获得了锁(针对同一资源)。针对这样场景,解决方式也很简单,也就是让Redis崩溃后延迟重启,并且这个延迟时间大于锁的过期时间就好。这样等节点重启后,所有节点上的锁都已经失效了。也不存在以上出现2个客户端获取同一个资源的情况了
还有一种情况,如果客户端1获取锁后,访问共享资源操作执行任务时间过长(要么逻辑问题,要么发生了GC),导致锁过期了,而后续客户端2获取锁成功了,这样就会导致客户端1和客户端2同时操作共享资源,相当于同一个时刻出现了2个客户端获得了锁的情况。这也就是上面锁过期时间要远远大于加锁消耗的时间的原因。
服务器台数越多,出现不可预期的情况也越多,所以针对分布式锁的应用的时候需要多测试。
如果系统对共享资源有非常严格要求得情况下,还是建议需要做数据库锁的方案来补充,如飞机票或火车票座位得情况。
对于一些抢购获取,针对偶尔出现超卖,后续可以通过人工介入来处理,毕竟redis节点不是天天奔溃,同时数据库锁的方案
性能又低。
9.2.2 实现
redisson包已经有对redlock算法封装
public interface DistributedLock {
/**
* 获取锁
* @author zhi.li
* @return 锁标识
*/
String acquire();
/**
* 释放锁
* @author zhi.li
* @param indentifier
* @return
*/
boolean release(String indentifier);
}
public class RedisDistributedRedLock implements DistributedLock {
/**
* redis 客户端
*/
private RedissonClient redissonClient;
/**
* 分布式锁的键值
*/
private String lockKey;
private RLock redLock;
/**
* 锁的有效时间 10s
*/
int expireTime = 10 * 1000;
/**
* 获取锁的超时时间
*/
int acquireTimeout = 500;
public RedisDistributedRedLock(RedissonClient redissonClient, String lockKey) {
this.redissonClient = redissonClient;
this.lockKey = lockKey;
}
@Override
public String acquire() {
redLock = redissonClient.getLock(lockKey);
boolean isLock;
try{
isLock = redLock.tryLock(acquireTimeout, expireTime, TimeUnit.MILLISECONDS);
if(isLock){
System.out.println(Thread.currentThread().getName() + " " + lockKey + "获得了锁");
return null;
}
}catch (Exception e){
e.printStackTrace();
}
return null;
}
@Override
public boolean release(String indentifier) {
if(null != redLock){
redLock.unlock();
return true;
}
return false;
}
}
10、热点缓存(热key)
10.1 场景
假设你现在有 10 个缓存节点来抗⼤量的读请求。正常情况下,读请求应该是均匀的落在 10 个缓存节点上的,这 10 个缓存节点,每秒承载 1 万请求是差不多的。
然后我们再做⼀个假设,你⼀个节点承载 2 万请求是极限,所以⼀般你就限制⼀个节点正常承载 1 万请求就 ok 了,稍微留⼀点 buffer 出来。
好,所谓的热点缓存问题是什么意思呢?
很简单,就是突然因为莫名的原因,出现⼤量的⽤户访问同⼀条缓存数据。
举个例⼦,某个明星突然宣布跟某某结婚,这个时候是不是会引发可能短时间内每秒都是数⼗
万的⽤户去查看这个明星跟某某结婚的那条新闻?
那么假设那条新闻就是⼀个缓存,然后对应就是⼀个缓存 key,就存在⼀台缓存机器上,此时
瞬时假设有 20 万请求奔向那⼀台机器上的⼀个 key。
我们刚才假设的是⼀个缓存 Slave 节点最多每秒就是 2 万的请求,当然实际缓存单机承载 5 万~ 10 万读请求也是可能的,我们这⾥就是⼀个假设。结果此时,每秒突然奔过来 20 万请求到这台机器上,会怎么样?
很简单,那台被 20 万请求指向的缓存机器会过度操劳⽽宕机的。
那么如果缓存集群开始出现机器的宕机,此时会如何?
接着,读请求发现读不到数据,会从数据库⾥提取原始数据,然后放⼊剩余的其他缓存机器⾥
去。但是接踵⽽来的每秒 20 万请求,会再次压垮其他的缓存机器。
以此类推,最终导致缓存集群全盘崩溃,引发系统整体宕机。
10.2 基于流式计算技术的缓存热点⾃动发现
这⾥关键的⼀点,就是对于这种热点缓存,你的系统需要能够在热点缓存突然发⽣的时
候,直接发现他,然后瞬间⽴⻢实现毫秒级的⾃动负载均衡。
那么我们就先来说说,你如何⾃动发现热点缓存问题?
⾸先你要知道,⼀般出现缓存热点的时候,你的每秒并发肯定是很⾼的,可能每秒都⼏⼗万甚
⾄上百万的请求量过来,这都是有可能的。
所以,此时完全可以基于⼤数据领域的流式计算技术来进⾏实时数据访问次数的统计,⽐如
storm、spark streaming、flink,这些技术都是可以的。
然后⼀旦在实时数据访问次数统计的过程中,⽐如发现⼀秒之内,某条数据突然访问次数超过
了 1000,就直接⽴⻢把这条数据判定为是热点数据,可以将这个发现出来的热点数据写⼊⽐如
zookeeper 中。
当然,你的系统如何判定热点数据,可以根据⾃⼰的业务还有经验值来就可以了。
当然肯定有⼈会问,那你的流式计算系统在进⾏数据访问次数统计的时候,会不会也存在说单
台机器被请求每秒⼏⼗万次的问题呢?
答案是否,因为流式计算技术,尤其是 storm 这种系统,他可以做到同⼀条数据的请求过来,
先分散在很多机器⾥进⾏本地计算,最后再汇总局部计算结果到⼀台机器进⾏全局汇总。
所以⼏⼗万请求可以先分散在⽐如 100 台机器上,每台机器统计了这条数据的⼏千次请求。
然后 100 条局部计算好的结果汇总到⼀台机器做全局计算即可,所以基于流式计算技术来进⾏
统计是不会有热点问题的
10.3 热点缓存⾃动加载为 JVM 本地缓存
我们⾃⼰的系统可以对 zookeeper 指定的热点缓存对应的 znode 进⾏监听,如果有变化他⽴⻢
就可以感知到了。
此时系统层就可以⽴⻢把相关的缓存数据从数据库加载出来,然后直接放在⾃⼰系统内部的本
地缓存⾥即可。
这个本地缓存,你⽤ ehcache、hashmap,其实都可以,⼀切都看⾃⼰的业务需求,主要说的
就是将缓存集群⾥的集中式缓存,直接变成每个系统⾃⼰本地实现缓存即可,每个系统⾃⼰本
地是⽆法缓存过多数据的。
因为⼀般这种普通系统单实例部署机器可能就⼀个 4 核 8G 的机器,留给本地缓存的空间是很
少的,所以⽤来放这种热点数据的本地缓存是最合适的,刚刚好。
假设你的系统层集群部署了 100 台机器,那么好了,此时你 100 台机器瞬间在本地都会有⼀份
热点缓存的副本。
然后接下来对热点缓存的读操作,直接系统本地缓存读出来就给返回了,不⽤再⾛缓存集群
了。
这样的话,也不可能允许每秒 20 万的读请求到达缓存机器的⼀台机器上读⼀个热点缓存了,⽽
是变成 100 台机器每台机器承载数千请求,那么那数千请求就直接从机器本地缓存返回数据
了,这是没有问题的。
10.4 限流熔断保护
除此之外,在每个系统内部,其实还应该专⻔加⼀个对热点数据访问的限流熔断保护措施。
每个系统实例内部,都可以加⼀个熔断保护机制,假设缓存集群最多每秒承载 4 万读请求,那
么你⼀共有 100 个系统实例。
你⾃⼰就该限制好,每个系统实例每秒最多请求缓存集群读操作不超过 400 次,⼀超过就可以
熔断掉,不让请求缓存集群,直接返回⼀个空⽩信息,然后⽤户稍后会⾃⾏再次重新刷新⻚⾯
之类的。
通过系统层⾃⼰直接加限流熔断保护措施,可以很好的保护后⾯的缓存集群、数据库集群之类
的不要被打死,我们来看看下⾯的图。