Redis为什么速度快
1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
2、数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;
3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
4、使用多路I/O复用模型,非阻塞IO;
5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
以上几点都比较好理解,下边我们针对多路 I/O 复用模型进行简单的探讨:
(1)多路 I/O 复用模型
多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。
这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈,主要由以上几点造就了 Redis 具有很高的吞吐量。
==select和epoll,poll的区别==
Redis 有哪些数据结构?
- 普通数据结构:
- 字符串 String
- 字典Hash
- 列表List
- 集合Set
- 有序集合 SortedSet
- 中级数据结构:
- HyperLogLog
- Geo
- Pub / Sub
- 高端数据结构:
- BloomFilter
- RedisSearch
- Redis-ML
- JSON
==Redis数据结构的应用场景==
有序集合底层原理
- 有序集合对象的编码可以是ziplist或者skiplist
- ziplist编码的有序集合对象底层实现是压缩列表,每个有序集合的元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存有序集合的元素,第二个节点保存元素的分值。压缩列表的集合元素按照分值从小到大开始排序,分值较小的节点靠近压缩列表的表头方向,分值较大的节点靠近压缩列表的表尾方向
- skiplist编码的有序集合对象使用zset作为底层实现,一个zset结构同时包含一个字典和一个跳跃表。zset结构中的zsl跳跃表按照分值从小到大保存了所有集合元素,每个跳跃表节点都保存了一个集合元素,跳跃表节点的object属性保存了元素的成员,score属性保存了元素的分支,通过跳跃表,程序可以对有序集合进行范围型操作,如ZRANK、ZRANGE等命令。zset结构中的dict字典为有序集合创建了一个从成员到分值的映射,字典的每一个键值对都保存了一个集合元素,字典的键保存了元素的成员,字典的值保存了元素的分值,通过这个字典,程序可以以O(1)复杂度查找给定成员的分值,ZSCORE命令就是根据这一特性实现的。
==跳跃表原理==
为什么有序集合需要同时使用跳跃表和字典来实现呢
理论上来讲,无论有序集合单独使用跳跃表和字典来实现有序集合,性能都会比同时使用跳跃表和字典有所降低。例如,如只使用字典来实现有序集合,那么在使用有序集合的ZRANK、ZRANGE命令时,程序都需要对字典保存的所有元素进行排序,完成这种排序的复杂度为O(nlog(n)),以及额外的O(N)的空间(创建数组保存排序后的元素);如只使用跳跃表实现有序集合,那么在根据指定成员查询分值时,复杂度就会变成O(log(n)),而不是字典的O(1)。综合以上原因,为了让有序集合同事拥有字典和跳跃表的所有特点,Redis选择同时使用跳跃表和字典来实现有序集合。
skiplist中的zset为什么不使用红黑数而使用跳跃表
简单分析一下skiplist的时间复杂度和空间复杂度,以便对于skiplist的性能有一个直观的了解。
我们先来计算一下每个节点所包含的平均指针数目(概率期望)。节点包含的指针数目,相当于这个算法在空间上的额外开销(overhead),可以用来度量空间复杂度。
跳跃表中产生越高的节点层数,概率越低。定量的分析如下:
- 节点层数至少为1。而大于1的节点层数,满足一个概率分布。
- 节点层数恰好等于1的概率为1-p。
- 节点层数大于等于2的概率为p,而节点层数恰好等于2的概率为p(1-p)。
- 节点层数大于等于3的概率为p^2,
而节点层数恰好等于3的概率为p^2(1-p)。 - 节点层数大于等于4的概率为p^3,
而节点层数恰好等于4的概率为p^3(1-p)。
因此,一个节点的平均层数(也即包含的平均指针数目),计算如下:
(1-p)+2p(1-p)+3p^2 (1-p)+4p^3 (1-)p+kp^(k-1) (1-p)=(1-)p·1/(1-p)^2=1/(1-p)
上述结果可以使用错位相减法计算得到。
现在很容易计算出:
- 当p=1/2时,每个节点所包含的平均指针数目为2;
- 当p=1/4时,每个节点所包含的平均指针数目为1.33。这也是Redis里的skiplist实现在空间上的开销。
==skiplist与平衡树、哈希表的比较==
- skiplist和各种平衡树(如AVL、红黑树等)的元素是有序排列的,而哈希表不是有序的。因此,在哈希表上只能做单个key的查找,不适宜做范围查找。所谓范围查找,指的是查找那些大小在指定的两个值之间的所有节点。
- 在做范围查找的时候,平衡树比skiplist操作要复杂。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在skiplist上进行范围查找就非常简单,只需要在找到小值之后,对第1层链表进行若干步的遍历就可以实现。
- 平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。
- 从内存占用上来说,skiplist比平衡树更灵活一些。一般来说,平衡树每个节点包含2个指针(分别指向左右子树),而skiplist每个节点包含的指针数目平均为1/(1-p),具体取决于参数p的大小。如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。
- 查找单个key,skiplist和平衡树的时间复杂度都为O(log n),大体相当;而哈希表在保持较低的哈希值冲突概率的前提下,查找时间复杂度接近O(1),性能更高一些。所以我们平常使用的各种Map或dictionary结构,大都是基于哈希表实现的。
- 从算法实现难度上来比较,skiplist比平衡树要简单得多。
Redis中的skiplist和经典有何不同
- 分数(score)允许重复,即skiplist的key允许重复。这在最开始介绍的经典skiplist中是不允许的。
- 在比较时,不仅比较分数(相当于skiplist的key),还比较数据本身。在Redis的skiplist实现中,数据本身的内容唯一标识这份数据,而不是由key来唯一标识。另外,当多个元素分数相同的时候,还需要根据数据内容来进字典排序。
- 第1层链表不是一个单向链表,而是一个双向链表。这是为了方便以倒序方式获取一个范围内的元素。
- 在skiplist中可以很方便地计算出每个元素的排名(rank)。
如何使用Redis进行限流
1、SortedSet
利用zset数据结构的score值来作为时间窗口,value保证唯一性即可,比如UUID或者雪花算法snowflake。
用一个zset结构记录用户的历史行为,每一个行为都会作为zset中的一个key保存下来,同一个用户的同一种行为用一个zset记录。
为节省内存,只保留时间窗口内的行为记录,如果用户是冷用户,窗口内的行为是空记录,则这个zset可以从内存中移除。
通过统计窗口内的行为数量与阈值进行比较就可以得出当前行为是否允许。
代码
public class SimpleRateLimiter {
private Jedis jedis;
public SimpleRateLimiter(Jedis jedis) {
this.jedis = jedis;
}
// 指定用户 user_id 的某个行为 action_key 在特定的时间内 period 只允许发生一定的次数 max_co
public boolean isActionAllowed(String userId, String actionKey, int period, int maxCount) {
String key = String.format("hist:%s:%s", userId, actionKey);
long nowTs = System.currentTimeMillis();
Pipeline pipe = jedis.pipelined();
pipe.multi();
// value 没有实际的意义,保证唯一就可以
pipe.zadd(key, nowTs, "" + nowTs);
pipe.zremrangeByScore(key, 0, nowTs - period * 1000);
Response<Long> count = pipe.zcard(key);
pipe.expire(key, period + 1);
pipe.exec();
pipe.close();
return count.get() <= maxCount;
}
public static void main(String[] args) {
Jedis jedis = new Jedis();
SimpleRateLimiter limiter = new SimpleRateLimiter(jedis);
for (int i = 0; i < 20; i++) {
// 调用这个接口 , 一分钟内只允许最多回复 5 个帖子
System.out.println(limiter.isActionAllowed("laoqian", "reply", 60, 5));
}
}
}
缺点:因为它要记录时间窗口内所有的行为记录, 如果这个量很大,比如“限定 60s 内操作不得超过 100 万次”之类,它是不适合做这样的限流的,因为会消耗大量的存储空间。
Redission版本
public static void main(String[] args) {
RedissonClient redisson = Redisson.create();
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
String id = "123";
RBucket<Boolean> bucket = redisson.getBucket("blackId:" + id);
// 是否是黑名单得
if(bucket.get() == true){
// 或者返回最近的一个请求的缓存
return;
}
long nanoTime = System.nanoTime();
RScoredSortedSet<Object> zset = redisson.getScoredSortedSet(id);
zset.expire(10, TimeUnit.SECONDS);
zset.add(nanoTime, nanoTime);
zset.removeRangeByScore(0, true,
nanoTime - 10 * 1000 * 1000 * 1000, true);
int size = zset.size();
// 超过了10次,则进入黑名单
if(size > 10){
// 加入黑名单,30秒之后不能再访问
bucket.set(true, 30, TimeUnit.SECONDS);
// 或者返回最近的一个请求的缓存
return;
}
// 放行
}
不使用Redis的单机Java版本
public class TestLimitFlow {
private Lock lock = new ReentrantLock();
private Map<String, Entity> map = new ConcurrentHashMap<>();
public TestLimitFlow() {
new Thread(() -> {
while (true) {
System.out.println(map.size());
map.forEach((key, value) -> {
long now = System.nanoTime();
if (value.expireTime < now) {
map.remove(key);
}
});
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
class Entity<T> {
public long expireTime;
T o;
}
public void interceptor(String ip) throws InterruptedException {
lock.lock();
Entity e = map.get(ip);
long nanoTime = System.nanoTime();
if (e == null) {
TreeSet<Long> treeSet = new TreeSet<>();
e = new Entity<TreeSet<Long>>();
e.expireTime = nanoTime + 10L * 1000 * 1000 * 1000;
e.o = treeSet;
map.put(ip, e);
}
lock.unlock();
synchronized (e) {
e.expireTime = nanoTime + 10L * 1000 * 1000 * 1000;
TreeSet<Long> zset = (TreeSet<Long>) e.o;
zset.add(nanoTime);
List<Long> deleteKey = Lists.newArrayList();
for(Long item : zset){
if(item < nanoTime - 10L * 1000 * 1000 * 1000){
System.out.println("true");
// zset.remove(item);
deleteKey.add(item);
}else {
break;
}
}
deleteKey.forEach(item -> {
zset.remove(item);
});
System.out.println(zset.size());
if (zset.size() > 10) {
// 加入黑名单
System.out.println("黑名单:" + ip + ":" + zset.size());
}
}
}
public static void main(String[] args) throws InterruptedException {
TestLimitFlow testXL = new TestLimitFlow();
for(int j = 0; j < 10;j++){
int finalJ = j;
new Thread(){
@Override
public void run() {
for (int i = 0; i < 12; i++) {
// TimeUnit.SECONDS.sleep(1);
try {
testXL.interceptor("localhost" + finalJ);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
}
2、令牌桶算法
如何使用 Redis 实现分布式锁?
分布式锁要解决的问题
- 互斥性
- 安全性
- 死锁
- 容错
版本1
使用 SEATNX key value:如果key不存在,则创建并赋值
@Service
public class RedisSeckillServiceImpl {
@Autowired
private StringRedisTemplate stringRedisTemplate;
public String deductStock() {
String lockKey = "lambo";
try {
String value = "value";
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, value);
if (!result) {
return "error_code";
}
// 此处机器宕机,将发生死锁
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} catch (Exception e) {
} finally {
stringRedisTemplate.delete(lockKey);
}
return "end";
}
}
分析
在setnx后如果机器宕机,锁得不到释放,就会产生死锁
版本2
给key设置过期时间
EXPIRE key seconds
@Service
public class RedisSeckillServiceImpl {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 设置过期时间,防止宕机带来的死锁
*
* @return
*/
public String deductStock2() {
String lockKey = "lambo";
try {
String value = "value";
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, value);
// 设置过期时间,但是set和expire不是原子性操作,还没有设置过期时间,机器宕机了
stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);
if (!result) {
return "error_code";
}
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} catch (Exception e) {
} finally {
stringRedisTemplate.delete(lockKey);
}
return "end";
}
}
缺点
原子性等不到满足,死锁问题依旧会出现
版本3
使用原子性操作
SETNX key value [EX seconds] [PX milliseconds] [NX|XX]
NX:只在键不存在时,才对键进行设置
XX:只在键存在时,才对键进行设置
@Service
public class RedisSeckillServiceImpl {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* set+expire变更成原子操作
*
* @return
*/
public String deductStock3() {
String lockKey = "lambo";
try {
String value = "value";
// 原子操作
Boolean result = stringRedisTemplate.opsForValue().setIfPresent(lockKey, value, 10, TimeUnit.SECONDS);
if (!result) {
return "error_code";
}
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} catch (Exception e) {
} finally {
// 线程可能会释放其他线程的锁,无法保证安全性
stringRedisTemplate.delete(lockKey);
}
return "end";
}
}
分析
比如线程1设置key10秒过期,执行业务需要15s,10秒后线程2进入,由于key过期自动删除后,线程2拿到锁,执行业务需要8秒,在第15秒的时候线程一将锁释放了,但是此时线程也还没操作完,这样其他线程又可以抢到锁了
版本4
给线程设置唯一值,释放锁的时候只有加锁的线程才可以释放锁
@Service
public class RedisSeckillServiceImpl {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 线程只能释放自己加的锁,不能释放别的线程加的锁
*
* @return
*/
public String deductStock4() {
String lockKey = "lambo";
String clientId = UUID.randomUUID().toString();
try {
// 原子操作
Boolean result =
// 设置唯一值
stringRedisTemplate.opsForValue().setIfPresent(lockKey, clientId, 10, TimeUnit.SECONDS);
if (!result) {
return "error_code";
}
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} catch (Exception e) {
} finally {
if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
stringRedisTemplate.delete(lockKey);
}
}
return "end";
}
}
分析
此时线程是只能释放自己加的锁了,无法释放其他线程的锁,但是有可能业务本身执行所需要的时间就是超过过期时间,而且这个时间是无法预估的,此时我们一种机制,每隔一段时间去判断线程是否还持有锁,如果持有锁则延长锁的过期时间。
版本5
使用redisson的看门狗机制
@Service
public class RedisSeckillServiceImpl {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedissonClient redissonClient;
/**
* 基于redisson设置分布式锁
*
* @return
*/
public String deductStock5() {
String lockKey = "lambo";
RLock redissonLock = redissonClient.getLock(lockKey);
try {
redissonLock.lock();
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} catch (Exception e) {
} finally {
redissonLock.unlock();
}
return "end";
}
}
如果Redis有1亿个key,使用keys命令是否会影响线上服务
首先keys pattern命令可以从redis中查找所有符合给定模式pattern的key
如果Redis有1亿个key,使用keys命令是会影响线上服务的,因为Redis是单线程模型,当然可以部署多个节点。
但是redis提供从海量数据里查询某一固定前缀的key的命令:
SCAN cursor [MATCH pattern] [COUNT count]
1、基于游标的迭代器。需要基于上一次的游标延续之前的迭代过程
2、以0作为游标开始一次新的迭代,直到命令返回游标0完成一次遍历
3、不保证每次执行都返回某个给定数量的元素,支持模糊查询
4、一次返回的数量不可控,只能是大概率符合count数
例如 scan 0 match k* count 1;
如何使用 Redis 实现消息队列?
一般使用 list 结构作为队列,rpush 生产消息,lpop 消费消息。当 lpop 没有消息的时候,要适当 sleep 一会再重试。
可不可以不用 sleep 呢?
list 还有个指令叫 blpop ,在没有消息的时候,它会阻塞住直到消息到来。
能不能生产一次消费多次呢?
使用 pub / sub 主题订阅者模式,发送者(pub)发送消息,订阅者(sub)接收消息。可以实现 1:N 的消息队列。
- 订阅命令
subscribe channelName - 发布命令
publish channelName value
pub / sub 有什么缺点?
在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如 rabbitmq 等。
如何实现延时队列?
使用 sortedset ,拿时间戳作为 score ,消息内容作为 key 调用 zadd 来生产消息,消费者用 zrangebyscore 指令获取 N 秒之前的数据轮询进行处理。
谈谈缓存击穿,缓存雪崩,缓存穿透
缓存击穿(单个key失效同时有大量请求)
定义
缓存中的key一般有设置过期时间,如果某个key过期了,在这个时候,有大量的并发请求访问这个key,则这写请求都会进入到DB中,导致DB瞬间压力过大,压垮DB
解决方案
1、设置互斥锁,mutex。当缓存失效的时候不是立即去访问数据库,而是使用分布式锁,比如redis,redission,zookeeper,
缺点
可能造成死锁,或者线程池阻塞
缓存穿透(key本身不存在于数据库)
定义
数据库中不存在的key,自然而然缓存中而不存在,如果有大量地访问不存在的key的请求,请求会直接到数据库,压垮数据库
解决方案
1、布隆过滤器
布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在与海量数据中。我们需要的就是判断 key 是否合法,有没有感觉布隆过滤器就是我们想要找的那个“人”。具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,我会先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。
具体可看这篇文章
https://github.com/Snailclimb/JavaGuide/blob/master/docs/dataStructures-algorithms/data-structure/bloom-filter.md
2、不存在的key,在缓存中的value值设置为null,并且设置过期时间,
java伪代码如下
public Object getObjectInclNullById(Integer id) {
// 从缓存中获取数据
Object cacheValue = cache.get(id);
// 缓存为空
if (cacheValue != null) {
// 从数据库中获取
Object storageValue = storage.get(key);
// 缓存空对象
cache.set(key, storageValue);
// 如果存储数据为空,需要设置一个过期时间(300秒)
if (storageValue == null) {
// 必须设置过期时间,否则有被攻击的风险
cache.expire(key, 60 * 5);
}
return storageValue;
}
return cacheValue;
缓存雪崩(大量key同时失效)
定义
缓存中大量的key在同一时间失效,这时候大量的请求就会直接到数据库,使数据库的压力过大
解决方案
1、在过期时间上添加随机值,把缓存失效的时间错开,这样的话失效的时间的重复率就降低了,降低了集体失效的概率
Redis 有几种数据“过期”策略?
Redis key过期的方式有三种:
首先删除策略有三种
定时删除
在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间来临,立即执行对键的删除操作
优点:对内存友好
缺点:对CPU不友好
惰性删除
放任键过期时间不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话就删除该键;如果没有过期就返回该键
优点:对CPU友好
缺点:对内存不友好,造成内存泄漏
定期删除
每隔一段时间,程序就对数据库进行一次检查没删除里面的过期键。至于要删除多少过期键,以及检查多少个数据库,则由算法决定
优点:是对定时删除和惰性删除的一种整合和折中
缺点:难以缺点删除操作执行的时长和频率。执行太频繁会退化成定时策略,执行太少又会退化成惰性删除策略
定时删除和定期删除为主动删除策略,惰性删除为被动删除策略
Redis删除策略
被动删除:当读/写一个已经过期的key时,会触发惰性删除策略,直接删除掉这个过期key
主动删除(定期删除策略):由于惰性删除策略无法保证冷数据被及时删掉,所以Redis会定期主动淘汰一批已过期的key
当前已用内存超过maxmemory限定时,触发主动清理策略(即下面的淘汰策略)
Redis 有哪几种数据“淘汰”策略?
==Redis 提供了 6 种数据淘汰策略:==
- volatile-lru 只对设置了过期时间的key进行LRU
- volatile-ttl 删除即将过期了的key
- volatile-random
只对设置了过期时间的key进行随机删除 - allkeys-lru
对所有key进行LRU算法删除 - allkeys-random
对所有key进行随机删除 - no-enviction
永不过期,直接抛错
Redis持久化机制有哪些?
Redis有两种持久化机制,AOF和RDB。
- AOF,记录每次写请求的命令,以追加的方式在文件尾部追加,直接在尾部追加,效率比较高。
对于操作系统来说,不是每次写都直接写到磁盘,操作系统自己会有一层cache,redis写磁盘的数据会先缓存在os cache里,redis每隔1秒调用一次操作系统的fsync操作,强制将os cache中的数据刷入AOF文件中。
当redis重启的时候,就把AOF中记录的命令重新执行一遍就可以了,但是如果文件很大的话,执行会耗费较多的时间,对于数据恢复来说耗时会多一点。
- RDB,是快照文件,每隔一定时间将redis内存中的数据生成一份完整的RDB快照文件,当redis重启的时候直接加载数据即可,同样的数据比AOF恢复的要快。
说说这两种持久化机制各自的特点、优缺点吧
==RDB优点==
第一点就是他会生成多个数据文件,每个数据文件都代表了某一时刻redis中的全量数据,非常适合做冷备。
第二点,RDB持久化机制对redis对外提供的读写服务影响非常小,可以让redis保持高性能,因为redis主进程只需要fork一个子进程,让子进程执行磁盘IO操作来进行RDB持久化即可。
第三点,相对于AOF持久化机制来说,直接基于RDB数据文件来重启和恢复redis进程,更加快速。
RDB,就是一份数据文件,恢复的时候,直接加载到内存中即可。
==RBD的缺点==
1)故障时可能数据丢失的比AOF要多。
可能会因为Redis挂起而且是从当前值最近一次快照期间的数据
这个问题,也是rdb最大的缺点,就是不适合做第一优先的恢复方案,如果你依赖RDB做第一优先恢复方案,会导致数据丢失的比较多
2)RDB每次在fork子进程来执行RDB快照数据文件生成的时候,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒,或者甚至数秒。RDB是对内存数据的全量同步,数据量大会由于I/O而影响性能
所以一般不要让RDB的间隔太长,否则每次生成的RDB文件太大了,对redis本身的性能可能会有影响的。
==AOF的优点==
AOF持久化,记录下除了查询以外的所有变更数据量的指令,以append的形式追加保存到AOF文件中
1)AOF可以更好的保护数据不丢失
一般AOF会每隔1秒,通过一个后台线程执行一次fsync操作,最多丢失1秒钟的数据。
每隔1秒,就执行一次fsync操作,保证os cache中的数据写入磁盘中。
redis进程挂了,最多丢掉1秒钟的数据.
2)AOF持久化性能高
AOF日志文件以append-only模式写入,所以没有任何磁盘寻址的开销,写入性能非常高,而且文件不容易破损,即使文件尾部破损,也很容易修复。
3)AOF日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。
因为在rewrite log的时候,会对其中的指令进行压缩,创建出一份需要恢复数据的最小日志出来。再创建新日志文件的时候,老的日志文件还是照常写入。当新的merge后的日志文件ready的时候,再交换新老日志文件即可。
4)AOF日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。
比如某人不小心用flushall命令清空了所有数据,只要这个时候后台rewrite还没有发生,那么就可以立即拷贝AOF文件,将最后一条flushall命令给删了,然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据。
==AOF的缺点==
(1)对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大
(2)AOF开启后,支持的写QPS会比RDB支持的写QPS低,因为AOF一般会配置成每秒fsync一次日志文件,当然,每秒一次fsync,性能也还是很高的。
如果你要保证一条数据都不丢,也是可以的,AOF的fsync设置成没写入一条数据,fsync一次,但是那样导致redis的QPS大幅度下降。
(3)以前AOF发生过bug,就是通过AOF记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来。
所以说,类似AOF这种较为复杂的基于命令日志/merge/回放的方式,比基于RDB每次持久化一份完整的数据快照文件的方式,更加脆弱一些,容易有bug。
不过AOF就是为了避免rewrite过程导致的bug,因此每次rewrite并不是基于旧的指令日志进行merge的,而是基于当时内存中的数据进行指令的重新构建,这样健壮性会好很多。
(4)唯一的比较大的缺点,其实就是做数据恢复的时候,会比较慢,做冷备不太合适。
AOF持久化过程
调用fork()创建一个子进程
子进程将新的AOF写到一个临时文件里,不依赖原来的AOF文件
主进程持续将新的变动写到内存和原来的AOF文件里
主进程获取子进程重新AOF的完成信息,往新AOF增量变动
使用新的AOF文件替换掉旧的AOF文件
==主从复制==
首先说一下Redis Sentinel是怎么工作的?重点描述一下故障转移的过程
Sentinel会以每秒一次的频率向所有与它创建了命令连接的实例(包括主服务器,从服务器,其他Sentinel在内)发送PING命令
如果一个实例在down-after-milliseconds毫秒内没有向Sentinel返回有效回复,则该Sentinel任务该实例处于下线状态,称为主观下线
当Sentinel从其他Sentinel那里收到足够数量的已下线的判断后,Sentinel就会将从服务器从主观下线判定为客观下线,并对主服务器进行故障转移操作
当一个主服务器被判断为客观下线后,监视这个下线主服务器的各个Sentinel会进行协商,选举出一个领头的Sentinel,并由它对下线主服务器进行故障转移操作。
故障转移操作主要步骤:
在已下线主服务器下的所有从服务器器里,挑选出一个从服务器,并将其转换成新的主服务器
让已下线主服务器下的其他从服务器改为复制新的主服务器
将已下线主服务器设置为新的主服务器的从服务器,当这个旧的主服务器重新上线时,它就会成为新的主服务器的从服务器
故障转移时会从剩下的slave选举一个新的master,被选举为master的标准是什么?
Sentinel会将主服务器和从服务器的信息保存在一个列表中
主服务器下线时,会删除列表中所有处于下线或者断线状态的从服务器
删除列表中所有最近5秒内没有回复过领头Sentinel的INFO命令的从服务器,
删除所有与已下线的主服务器连接断开炒货down-after-milliseconds毫秒的从服务器,
然后根据优先级(从高到低),复制偏移量(从高到低),运行id(从小到大)进行排序选出领头的Sentinel
执行切换的那个哨兵在完成故障转移后会做什么?
会进行configuraiton配置信息传播。
哨兵完成切换之后,会在自己本地更新生成最新的master配置,然后通过pub/sub消息机制同步给其他的哨兵。
==Redis集群==