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一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储
![](https://img.haomeiwen.com/i13194828/b0d2d074004031da.png)
AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录
![](https://img.haomeiwen.com/i13194828/cca85efba741f264.png)
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、什么是缓存命中率?提高缓存命中率的方法有哪些??
- 命中:可以直接通过缓存获取到需要的数据
- 不命中:无法直接通过缓存获取到想要的数据,需要再次查询数据库或者执行其他的操作,原因可能是由于缓存中根本不存在,或者缓存已经过期
命中率越高表示使用缓存作用越好,性能越高(响应时间越短,吞吐量越高),并发能力也越好。
重点关注访问评率高且时效性相对低一些的业务数据上,利用预加载(预热)、扩容、优化缓存丽都。更新缓存等手段来提高命中率
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)
![](https://img.haomeiwen.com/i13194828/9cc55d93cf993b37.png)
注: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;
}
}
网友评论