Redis

基础内容

1. Redis是什么

Redis 是有C语言开发的高性能的非关系型数据库。

2. Redis的存储结构

2.1 String 

键值对。引用场景:缓存,短信验证码等

set  key  value //设置值

get  key   //获取值

2.2 Hash

键值对,可以存放多个键值对,适合存储对象。应用场景:缓存等,比String 更节省空间

hset  key  field  value //设置值

hget  key  field  //获取值

2.3 List 

相当于链表。应用场景:消息队列等

LPUSH key element1 element2 element3 ... //左边插入元素,头插法

LPOP key  //移除并返回列表左侧的第一个元素,没有则返回 nil

2.4 Set

HashSet 类似,无序,不可重复。应用场景:共同好友等

SADD  key  member1  member2  member3 ... //向set中添加一个或多个元素

SREM  key  member1  //移除set中的指定元素

2.5 Zset

类似 TreeSet,不可重复,有一个权重参数,按这个排序。应用场景:排行榜

ZADD key score1 member1 score2 member2  //添加一个或多个元素到 sorted set,如果已经存在则更新其 score值。

ZREM key  member1  //删除sorted  set 中的一个指定元素。

3. 为什么要用Redis和Redis为什么那么快

1. 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是O(1).

2. 数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的。

3. 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁的操作,没有因为可能出现死锁而导致的性能消耗。

4. 使用多路 I/O 复用模型,非阻塞 IO。

5. 使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis 直接自己构建了 VM 机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。

4. 缓存雪崩

缓存中的数据大量过期,然后大量请求取访问数据库,导致数据库压力过大或者宕机。或者 Redis宕机。

解决方式:

        使缓存过期时间分布比较均匀

        设置高可用集群

5. 缓存穿透

缓存和数据库都没有要的数据,然后会一直请求,给数据库压力。

解决:

        给缓存中设置一个空值或者默认值

        使用布隆过滤器,先通过过滤器判断数据是否存在,存在再继续向下查

6. 缓存击穿

再Redis 和数据库中有个热点Key,但是再大量用户请求途中,该热点Key过期了,然后再缓存中拿不到了,大量的请求去访问数据库,导致数据库的压力过大。

解决:

        设置缓存永不过期

        使用互斥锁,只让一个请求去访问,然后把数据库中的数据带到缓存中,然后剩余请求再去访问缓存中的数据。

7. Redis的持久化机制

7.1 RDB

RDB(默认):一定时间内将内存数据以快照(记录某一个时刻数据,相当于拍了一张照片)形式保存到硬盘中。某个时间点把数据写到临时文件,然后替换上次持久化的文件。

优点: 恢复大的数据集时,比AOF效率更高。

缺点:不安全,数据丢失。

7.2 AOF

AOF: 会把每次写的命令记录到日志文件(同一个日志文件,不会替换),Redis重启会将持久化的日志文件恢复。如果两种持久化都开启,优先恢复AOF。

优点:安全,几乎不会丢失

缺点: AOF 文件比RDB文件大,且恢复速度慢。

8. Redis的过期策略

定时删除:每个Key 都需要创建一个定时器,到时候就会清除 Key,所以对内存很友好,但是会占用CPU的大量资源去处理过期。

惰性删除:用到Key 的时候再去判断Key  有没有过期,过期就清除,可以节省 CPU资源,但是对内存不友好,可能出现大量过期的Key没有清除。

定期删除:定时删除和惰性删除的结合体,每隔一段时间抽取设置过期的key检测是否过期(默认是 1S 执行10次清除,每抽取5个进行检测),过期就清除。

        

# 1s检查10次

hz 10

# 一次抽取的个数,默认是5个

maxmemory-samples 5 

9. Redis 的淘汰策略

Redis 内存满了,进行内存淘汰,删除一部分key。 Redis4.0 添加的ifu策略。

#默认的最大内存是1G

maxmemory  1GB

        

volatile-lru:针对设置了过期时间的key,使用lru算法(最近最少使用key:根据时间)进行淘汰
allkeys-lru:针对所有key使用lru算法进行淘汰
volatile-ifu:针对设置了过期时间的key,使用ifu算法(最近最少使用:根据计数器)进行淘汰
allkeys-ifu:针对所有key使用ifu算法进行淘汰
volatile-random:从所有设置了过期时间的key中使用随机淘汰机制进行淘汰
allkeys-random:针对所有的key使用随机淘汰机制进行淘汰
volatile-ttl:删除过期时间最早(剩余存货时间最短的)
no-eviction(默认):不删除键,操作报错

10. Redis怎么设置高可用或者集群

主从复制:一个主,一个或者多个从,从节点在主节点复制数据,主节点负责读,可以更好的分担主节点的压力,但是如果主节点宕机了,会导致部分数据不同步

哨兵模式: 也是一种主从模式,哨兵定时去查询主机,如果主机长时间没有进行响应,多个哨兵就会通过投票算法从从机中投出新的主机,提高可用性,但是在选举新的主节点期间,还是不能正常工作。

cluster集群模式:采用多主多从(一般都是一主三从),按照规则进行分片,每台redis节点存储的数据不一样,解决了单机存储的问题。还提供了复制和故障转移功能,但是配置比较麻烦。

11.Redis实现分布式锁

分布式锁:是控制分布式系统不同进程共同访问共享资源的一种锁的实现。秒杀下单,抢红包等等业务场景,都需要用到分布式锁,Redis可以当分布式锁。

  • 「互斥性」: 任意时刻,只有一个客户端能持有锁。
  • 「锁超时释放」:持有锁超时,可以释放,防止不必要的资源浪费,也可以防止死锁。
  • 「可重入性」:一个线程如果获取了锁之后,可以再次对其请求加锁。
  • 「高性能和高可用」:加锁和解锁需要开销尽可能低,同时也要保证高可用,避免分布式锁失效。
  • 「安全性」:锁只能被持有的客户端删除,不能被其他客户端删除。

Redis分布式锁方案一:SETNX + EXPIRE

先用 setnx 进行抢锁,如果抢到之后,再用 expire 给锁设置一个过期时间,防止锁忘记了释放。

实现

sentx key value //新增一个key,key存在返回1,不存在返回0
expire key 100 //key过100秒过期,单位是秒
问题:无法保证原子性,sentx锁了之后,过期时间没有设置,那么锁就是永久的了(别的线程永远获取不到锁)

举例

假设某电商网站的某商品做秒杀活动,key可以设置为key_resource_id,value设置任意值

if(jedis.setnx(key_resource_id,lock_value) == 1){ //加锁
    expire(key_resource_id,100); //设置过期时间
    try {
        do something  //业务请求
    }catch(){
  }
  finally {
       jedis.del(key_resource_id); //释放锁
    }
}

但是这个方案中,setnx 和 expire 两个命令分开了,【不是原子操作】。如果执行完 setnx 加锁,正要执行 expire 设置过期时间时,进程crash 或者 重启进行维护了,那么这个锁就长生不老了。【别的线程永远获取不到这个锁】。

Redis 分布式锁方案二:SETNX + value值是(系统时间 + 过期时间)

为了解决sentx + expire 中发生异常锁得不到释放的场景,可以把过期时间放到sentx的value值里面。如果加锁失败,再拿出value值校验一下即可。

实现

setnx key 过期时间 //把过期时间放到value,解决过期时间没有设置的问题,对比过期时间和当前时间就可以知道是否过期
问题:取当前时间,分布式要求时间同步

举例

long expires = System.currentTimeMillis() + expireTime; //系统时间+设置的过期时间
String expiresStr = String.valueOf(expires);

// 如果当前锁不存在,返回加锁成功
if (jedis.setnx(key_resource_id, expiresStr) == 1) {
        return true;
} 
// 如果锁已经存在,获取锁的过期时间
String currentValueStr = jedis.get(key_resource_id);

// 如果获取到的过期时间,小于系统当前时间,表示已经过期
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {

     // 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间(不了解redis的getSet命令的小伙伴,可以去官网看下哈)
    String oldValueStr = jedis.getSet(key_resource_id, expiresStr);
    
    if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
         // 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才可以加锁
         return true;
    }
}
        
//其他情况,均返回加锁失败
return false;
}

这个方案的优点是,巧妙移除 expire 单独设置过期时间的操作,把【过期时间放到setnx的value值】里面来。解决了sentx + expire 发生异常,锁得不到释放的问题。

缺点

  • 过期时间是客户端自己生成的(System.currentTimeMillis()是当前系统的时间),必须要求分布式环境下,每个客户端的时间必须同步。
  • 如果锁过期的时候,并发多个客户端同时请求过来,都执行jedis.getSet(),最终只能有一个客户端加锁成功,但是该客户端锁的过期时间,可能被别的客户端覆盖
  • 该锁没有保存持有者的唯一标识,可能被别的客户端释放/解锁。

Redis分布式锁方案三:使用Lua脚本(包含Sentx + expire两条指令)

使用Lua脚本来保证原子性(包含sentx + expire两条指令:保证原子性 )

if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then
   redis.call('expire',KEYS[1],ARGV[2])
else
   return 0
end;
 String lua_scripts = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then" +
            " redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end";   
Object result = jedis.eval(lua_scripts, Collections.singletonList(key_resource_id), Collections.singletonList(values));
//判断是否成功
return result.equals(1L);

Redis分布式锁方案方案四:SET的扩展命令(SET EX PX NX)

除了使用,使用Lua脚本,保证SETNX + EXPIRE两条指令的原子性,我们还可以巧用Redis的SET指令扩展参数!(SET key value[EX seconds][PX milliseconds][NX|XX]),它也是原子性的!

        

EX seconds:将键的过期时间设置为 seconds 秒。
PX milliseconds:将键的过期时间设置为 milliseconds 毫秒。
NX:只在键不存在的时候,才对键进行设置操作。SET key value NX 等同于 SETNX key value
XX:只在键已经存在的时候,才对键进行设置操作

set key value ex 10 nx  //key不存在才能新增,并且设置过期时间为10s。
问题:(1) 锁过期释放了,业务还没执行完。a获取锁,执行时间超过10秒,然后被b线程获取锁,导致代码执行顺序不一致。
     (2)  锁被误删,因为(1),a没执行完释放锁,被b获取,然后a执行完,删除锁,然后b可能还没执行完。

if(jedis.set(key_resource_id, lock_value, "NX", "EX", 100s) == 1){ //加锁
    try {
        do something  //业务处理
    }catch(){
  }
  finally {
       jedis.del(key_resource_id); //释放锁
    }
}

缺点

  • 问题一:「锁过期释放了,业务还没执行完」。假设线程a获取锁成功,一直在执行临界区的代码。但是100s过去后,它还没执行完。但是,这时候锁已经过期了,此时线程b又请求过来。显然线程b就可以获得锁成功,也开始执行临界区的代码。那么问题就来了,临界区的业务代码都不是严格串行执行的啦。
  • 问题二:「锁被别的线程误删」。假设线程a执行完后,去释放锁。但是它不知道当前的锁可能是线程b持有的(线程a去释放锁时,有可能过期时间已经到了,此时线程b进来占有了锁)。那线程a就把线程b的锁释放掉了,但是线程b临界区业务代码可能都还没执行完呢。

Redis分布式锁方案方案五:SET EX PX NX  + 校验唯一随机值,再删除

既然锁可能被别的线程误删,那我们给value值设置一个标记当前线程唯一的随机数,在删除的时候,校验一下。

if(jedis.set(key_resource_id, uni_request_id, "NX", "EX", 100s) == 1){ //加锁
    try {
        do something  //业务处理
    }catch(){
  }
  finally {
       //判断是不是当前线程加的锁,是才释放
       if (uni_request_id.equals(jedis.get(key_resource_id))) {
        jedis.del(lockKey); //释放锁
        }
    }
}

在这里,「判断是不是当前线程加的锁」「释放锁」不是一个原子操作。如果调用jedis.del()释放锁的时候,可能这把锁已经不属于当前客户端,会解除他人加的锁。

if (uni_request_id.equals(jedis.get(key_resource_id))) {
        jedis.del(lockKey); //释放锁
        }           这一步是非原子性的

为了更严谨,一般也是用lua脚本代替。lua脚本如下:

if redis.call('get',KEYS[1]) == ARGV[1] then 
   return redis.call('del',KEYS[1]) 
else
   return 0
end;

Redis分布式锁方案六:Redisson框架

方案五还是可能存在「锁过期释放,业务没执行完」的问题。有些小伙伴认为,稍微把锁过期时间设置长一些就可以啦。其实我们设想一下,是否可以给获得锁的线程,开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放。

当前开源框架Redisson解决了这个问题。我们一起来看下Redisson底层原理图:

只要线程一加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程1还持有锁,那么就会不断的延长锁key的生存时间。因此,Redisson就是使用Redisson解决了「锁过期释放,业务没执行完」问题。

Redis分布式锁方案七:多机实现的分布式锁Redlock+Redisson

前面六种方案都只是基于单机版的讨论,还不是很完美。其实Redis一般都是集群部署的:

如果线程一在Redis的master节点上拿到了锁,但是加锁的key还没同步到slave节点。恰好这时,master节点发生故障,一个slave节点就会升级为master节点。线程二就可以获取同个key的锁啦,但线程一也已经拿到锁了,锁的安全性就没了。

为了解决这个问题,Redis作者 antirez提出一种高级的分布式锁算法:Redlock。Redlock核心思想是这样的:

搞多个Redis master部署,以保证它们不会同时宕掉。并且这些master节点是完全相互独立的,相互之间不存在数据同步。同时,需要确保在这多个master实例上,是与在Redis单实例,使用相同方法来获取和释放锁。

我们假设当前有5个Redis master节点,在5台服务器上面运行这些Redis实例。

RedLock的实现步骤:如下

 
  • 1.获取当前时间,以毫秒为单位。
  • 2.按顺序向5个master节点请求加锁。客户端设置网络连接和响应超时时间,并且超时时间要小于锁的失效时间。(假设锁自动失效时间为10秒,则超时时间一般在5-50毫秒之间,我们就假设超时时间是50ms吧)。如果超时,跳过该master节点,尽快去尝试下一个master节点。
  • 3.客户端使用当前时间减去开始获取锁时间(即步骤1记录的时间),得到获取锁使用的时间。当且仅当超过一半(N/2+1,这里是5/2+1=3个节点)的Redis master节点都获得锁,并且使用的时间小于锁失效时间时,锁才算获取成功。(如上图,10s> 30ms+40ms+50ms+4m0s+50ms)
  • 如果取到了锁,key的真正有效时间就变啦,需要减去获取锁所使用的时间。
  • 如果获取锁失败(没有在至少N/2+1个master实例取到锁,有或者获取锁时间已经超过了有效时间),客户端要在所有的master节点上解锁(即便有些master节点根本就没有加锁成功,也需要解锁,以防止有些漏网之鱼)。

简化下步骤就是:

  • 按顺序向5个master节点请求加锁
  • 根据设置的超时时间来判断,是不是要跳过该master节点。
  • 如果大于等于3个节点加锁成功,并且使用的时间小于锁的有效期,即可认定加锁成功啦。
  • 如果获取锁失败,解锁!

12. 分布式锁的特性

互斥性:任意时刻,只有一个客户端能持有锁。

锁超时释放:持有锁超时,可以释放,防止不必要的资源浪费,也可以防止死锁。

可重入性:一个线程如果获得锁之后,可以再次对其请求加锁。

高性能和高可用:加锁和解锁需要开销尽可能低,同时也要保证高可用,避免分布式锁失效。

安全性:锁只能被持有的客户端删除,不能被其他客户端删除。

13. Redis的应用场景

缓存

        将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率。

数据库

排行榜

计数器

        可以对 String 进行自增自减运算,从而实现计数器功能。Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的技术量。

消息队列

        List 是一个双向链表,可以通过 IPush 和 rpop 写入和读取消息。不过最好使用 kafka,RabbitMq等消息中间件。

分布式锁

        在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。可以使用Redis自带的 Setnx 命令实现分布式锁,除此之外,还可以使用官方提供的RedLock分布式锁实现。

共享session

14. Redis 有哪些优缺点?

优点

        读写性能优异,Redis能读的速度是110000次/秒,写的速度是81000次/秒。

        支持数据持久性,支持AOF和RDB两种持久化方法。

        支持事务,Redis 的所有操作都是原子性的,同时 Redis 还支持对几个操作合并后的原子性执行。

        数据结构丰富,除了支持String类型的Value外还支持hash、set、zset、list等数据结构。

        支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。

缺点

        数据库容量收到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。

        Redis不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复。

        主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引起数据不一致的问题,降低了系统的可用性。

        Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。为避免这一问题,运维人员在系统上线时必须确保有足够的空间,这对资源造成了很大的浪费。

15. 使用Redis有哪些好处?

  1. 速度快,因为数据存在内存中,类似于HashMap,HashMap 的优势就是查找和操作的时间复杂度都很低。
  2. 支持丰富数据类型,支持String,list,set,sorted set,hash.
  3. 支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行。
  4. 丰富的特性:可用于缓存,消息,按Key设置过期时间,过期后将会自动删除。

16. 为什么要用Redis,为什么要用缓存

主要从“高性能”和“高并发”这两点来看待这个问题。

高性能:

        假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变之后,同步改变缓存中相应的数据即可。

高并发:

        直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不经过数据库。

17. 为什么要用Redis 而不用 map/guava 做缓存?

缓存分为本地缓存和分布式缓存。以Java为例,使用自带的 Map 或者 Guava 实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 JVM 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。

使用Redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持 Redis 或 memcached 服务的高可用,整个程序架构上较为复杂。

18. Redis 持久化数据和缓存怎么做扩容

如果Redis被当做缓存使用,使用一致性哈希实现动态扩容缩容。

如果Redis 被当做一个持久化储存使用,必须使用固定的keys-to-nodes映射关系,节点的数量一旦确定不会变化。否则的话(即Redis 节点需要动态变化的情况),必须使用可以在运行时进行数据再平衡的一套系统,而当前只有Redis集群可以做到这样。

19. Redis key的过期时间和永久有效分别是怎么设置?

expire 和 persist 命令

20. 通过expire 来设置key 的过期时间,那么对过期的数据怎么处理?

除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:
1、定时去清理过期的缓存;
2、当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。

两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。

21.  Redis 如何做内存优化?

可以好好利用 Hash,list,sorted set,set 等集合类型数据,因为通过情况下很多小的 Key-Value 可以用更紧凑的方式存放到一起。尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的web系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的key,而是应该把这个用户的所有信息存储到一张散列表里面。

22. Redis线程模型

Redis 基于 Reactor 模式开发了网络事件处理器,这个处理器被称为文件事件处理器(file event handler)。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器 队列的消费是单线程的,所以Redis 才叫单线程模型。

文件事件处理器使用IO多路复用程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不用的事件处理器。

当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。

虽然文件事件处理器以单线程方式运行,但通过使用I/O 多路复用来监听多个套接字,文件事件处理器即实现了高性能的网络通信模型,又可以很好地与Redis 服务器中其他同样以单线程方式运行地模块进行对接,这保持了Redis 内部单线程设计的简单性。

Redis事务

1. 什么是事务?

  • 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行过程中,不会被其他客户端发送来的命令请求所打断。
  • 事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。

2. Redis 事务的概念

  • Redis 事务的本质是通过 MULTI、EXEC、WATCH 等一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
  • 总结说:Redis 事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。

3. Redis 事务的三个阶段

1. 事务开始 MULTI

2. 命令入队

3. 事务执行 EXEC

        

事务执行过程中,如果服务端收到 EXEC、DISCARD、WATCH、MULTI之外的请求,将会把请求放入队列中排队。

4. Redis 事务相关命令

Redis 事务功能是用过 MULTI、EXEC、DISCARD 和 WATCH 四个命令实现的。

        

Redis 会将一个事务中的所有命令序列化,然后按顺序执行。

        1. Redis 不支持回滚,“Redis 在事务失败时不进行回滚,而是继续执行余下的命令”,所以Redis的内部可以保持简单且快速。

        2.如果在一个事务中的命令出现错误,那么所有的命令都不会执行。

        3.如果在一个事务中出现运行错误,那么正确的命令会被执行。

        

WATCH 命令是一个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)行为。可以监控一个或多个建,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。

        

MULTI 命令用于开启一个事务,它总是返回OK。MULTI 执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当 EXEC 命令被调用时,所有队列中的命令才会被执行。

        

EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值 nil。

        

通过调用 DISCARD,客户端可以清空事务队列,并放弃执行事务,并且客户端会从事务状态中退出。

        

UNWATCH 命令可以取消watch对所有的监控。

5. 事务管理(ACID)概述

  • 原子性(Atomicity) 原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。

  • 一致性(Consistency) 事务前后数据的完整性必须保持一致。

  • 隔离性(Isolation) 多个事务并发执行时,一个事务的执行不应影响其他事务的执行

  • 持久性(Durability) 持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响

Redis的事务总是具有ACID中的一致性和隔离性,其他特性是不支持的。当服务器运行在_AOF_持久化模式下,并且appendfsync选项的值为always时,事务也具有耐久性。

6. Redis 事务支持隔离性吗?

Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。因此,Redis 的事务是总是隔离性的。

7. Redis 事务保证原子性吗,支持回滚吗

Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其他的命令仍会执行。

8. Redis 事务其他实现

  • 基于 Lua 脚本,Redis 可以保证脚本内的命令一次性、按顺序地执行,其同时也不提供事务运行错误的回滚,执行过程中如果部分命令运行错误,剩下的命令还是会继续运行完。
  • 基于中间标记变量,通过另外的标记变量来标识事务是否执行完成,读取数据时先读取该标记变量判断是否事务执行完成。但这样会需要额外写代码实现,比较繁琐。

集群方案

1. 哨兵模式

哨兵的介绍

sentinel,中文名是哨兵。哨兵是 Redis 集群机构中非常重要的一个组件,主要有以下功能:

  • 集群监控:负责监控 Redis master 和 slave 进程是否正常工作。
  • 消息通知:如果某个 Redis 实例有故障,那么哨兵负责发送消息作为报警通过给管理员。
  • 故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。
  • 配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。

        

哨兵用于实现 Redis 集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作。

  • 故障转移时,判断一个 master node 是否宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题。
  • 即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的,因为如果一个作为高可用机制重要组成部分的故障转移系统本身是单点的,那就GG了。

哨兵的核心知识

  • 哨兵至少需要3个实例,来保证自己的健壮性。
  • 哨兵 + Redis 主从的部署架构,是不保证数据零丢失的,只能保证 Redis 集群的高可用性。
  • 对于哨兵 + Redis 主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练。

2.官方Redis Cluster 方案(服务端路由查询)

问题1:

Redis 集群模式的工作原理能说一下吗?

Redis Cluster 是 Redis 官方提供的分布式解决方案,核心目标是:数据分片 + 高可用 + 自动故障转移

1. 核心结构

  • 整个集群被划分为 16384 个哈希槽(slot)
  • 每个主节点负责一部分槽
  • 每个主节点可以配 1~N 个从节点(replica)
  • 集群至少需要 3 主 才能正常运行(防止脑裂)

2. 工作流程

  1. 启动时:节点通过 Gossip 协议互相通信,交换集群信息,形成集群视图
  2. 槽分配:管理员或工具将 16384 个槽分配到各个主节点
  3. 数据读写:客户端计算 key 对应的槽,直接访问对应节点
  4. 故障检测:节点间互相 ping,超过半数认为节点主观下线 → 标记客观下线
  5. 故障转移:从节点竞选主节点,接管槽,更新集群信息

3. 关键特性

  • 无中心架构,不需要中间代理
  • 自动分片、自动迁移、自动故障转移
  • 客户端直连节点,性能高
  • 不支持跨节点事务、不支持多 key 批量操作(除非在同一个槽)

问题2:

集群模式下 key 如何寻址?

Redis Cluster 使用 哈希槽 + CRC16 算法 定位 key:

1. 寻址公式

        

HASH_SLOT = CRC16(key) & 16383
  • 对 key 做 CRC16 哈希
  • 对 16384 取模,得到 0~16383 之间的槽号
  • 集群维护槽 → 节点的映射表
  • 客户端缓存映射表,直接路由到对应节点

2. 强制同一个槽:Hash Tag

如果希望多个 key 落在同一个槽,可以用 {}

        

user:{100}:name
user:{100}:age

只对 100 做哈希,保证在同一个槽,可执行多 key 命令。

问题3:

常见分布式寻址算法?

1. 取模哈希(简单哈希)

nodeIndex = hash(key) % nodeCount
  • 优点:简单、均匀
  • 缺点:节点增减时几乎所有 key 都重新分布,缓存雪崩

2. 一致性哈希(Consistent Hashing)

  • 把节点哈希到 0~2³² 环上
  • key 也哈希到环上,顺时针找最近节点
  • 增减节点只影响一小部分数据

3. 虚拟节点哈希

  • 给每个物理节点分配多个虚拟节点,散列到环上
  • 解决一致性哈希节点少、分布不均问题

4. 哈希槽分片(Redis Cluster 使用)

  • 固定 16384 个槽
  • 槽分配给节点
  • 扩缩容只需迁移槽,key 与槽映射不变
  • 最适合分布式缓存 / 数据库集群

5. 范围分片

按 key 范围分区,如:

  • 0~100 万 → node1
  • 100 万~200 万 → node2适合有序数据,如 TiDB、HBase 等。

问题4:

一致性 Hash 算法详解?

1. 核心思想

  • 构造一个 0 ~ 2³² 的哈希环
  • 服务器节点通过哈希放到环上
  • key 也哈希到环上
  • 顺时针找第一个遇到的节点负责该 key

2. 优点

  • 增减节点只影响相邻一小部分数据
  • 不会像取模那样全局洗牌
  • 适合缓存、分布式存储

3. 缺点

  • 节点少时分布容易不均匀
  • 解决方法:引入虚拟节点

4. 虚拟节点

  • 一个真实节点对应多个虚拟节点(如 100~500 个)
  • 虚拟节点散落在环上
  • 数据分布更均匀,迁移更平滑

5. 与 Redis 槽的区别

  • 一致性哈希:环动态,节点变化影响局部
  • Redis 槽:固定 16384 槽,迁移更可控、更易运维Redis 没有用一致性哈希,而是用更工程化的哈希槽。

2.1 简介

Redis Cluster 是一种服务端 Sharding 技术,3.0 版本开始正式提供。Redis Cluster 并没有使用一致性hash,而是采用slot(槽)的概念,一共分成 16384 个槽。将请求发送到任意节点,接收到请求的节点会将查询请求发送到正确的节点上执行。

2.2 方案说明

1. 通过哈希的方式,将数据分片,每个节点均分存储一定哈希槽(哈希值)区间的数据,默认分配了 16384 个槽位。

2. 每份数据分片会存储在多个互为主从的多节点上。

3. 数据写入先写主节点,再同步到从节点(支持配置为阻塞同步)

4.同一分片多个节点间的数据不保持一致性。

5.读取数据时,当客户端操作的key没有分配再该节点上时,Redis 会返回转向指令,指向正确的节点。

6.扩容时需要把旧节点的数据迁移一部分到新节点。

        

在 Redis Cluster 架构下,每个 Redis 要放开两个端口号,比如一个是6379,另外一个就是 加1W 的端口号,比如16379。

        16379 端口号是用来进行节点间通信的,也就是 cluster bus 的东西,cluster bus 的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus 用了另外一种二进制的协议,gossip 协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。

2.3 节点间的内部通信机制

  • 基本通信原理
  • 集群元数据的维护有两种方式:集中式、Gossip 协议。Redis cluster 节点间采用 gossip 协议进行通信。

2.4 分布式寻址算法

  • hash 算法(大量缓存重建)
  • 一致性 hash 算法(自动缓存迁移)+ 虚拟节点(自动负载均衡)
  • Redis cluster 的 hash slot算法

2.5 优点与缺点

优点:

  • 无中心架构,支持动态扩容,对业务透明
  • 具备Sentinel 的监控和自动 Failover (故障转移)能力
  • 客户端不需要连接集群所有节点,连接集群中任何一个可用节点即用
  • 高性能,客户端直连Redis服务,免去了Proxy 代理的损耗

缺点

  • 运维也很复杂,数据迁移需要人工干预
  • 只能使用0号数据库
  • 不支持批量操作(pipeline管道操作)
  • 分布式逻辑和存储模块耦合等

3. 基于客户端分配

3.1 简介

Redis sharding 是 Redis Cluster 出来之前,业界普遍使用的多 Redis 实例集群方法。其主要思想是采用哈希算法将Redis 数据的key 进行散列,通过 Hash 函数,特定的 Key 会映射到特定的 Redis 节点上。Java Redis 客户端驱动Jedis,支持 Redis Sharding 功能,即 ShardedJedis 以及 结合缓存池的 ShardedJedisPool。 

3.2 优点和缺点

优点:

        优势在于非常简单,服务器的 Redis 实例彼此独立,相互无关联,每个 Redis 实例像单服务器一样运行,非常容易线性扩展,系统的灵活性很强。

缺点:

         由于 Sharding 处理放到客户端,规模进一步扩大时给运维带来挑战。

        客户端sharding 不支持动态增删节点。服务器Redis 实例群拓拔结构有变化时,每个客户端都需要更新调整。连接不能共享,当应用规模增大时,资源浪费制约优化。

4. 基于代理服务器分片

4.1 简介

客户端发送请求到一个代理组件,代理解析客户端的数据,并将请求转发到正确的节点,最后将结果回复给客户端。

4.2 特征

  • 透明接入,业务程序不要关心后端Redis实例,切换成本低
  • Proxy 的逻辑和存储的逻辑时隔离的。
  • 代理层多一次转发,性能有所损耗。

4.3 业界开源方案

  • Twtter 开源的 Twemproxy
  • 豌豆荚开源的Codis

5. Redis 主从架构

单机的Redis,能够承载的QPS大概就在上万到几万不等。对于缓存来说,一般都是用来支撑读高并发的。因此架构做成主从架构,一主多从,主负责写,并且将数据复制到其他的slave节点。从节点负责读,所有的读请求全部走从节点。这样也可以很轻松实现水平扩容,支撑读高并发。

Redis replication   --------------->   主从架构  ----------------->  读写分离  --------------------->  水平扩容支撑读高并发

5.1 Redis Replication 的核心机制

  • Redis 采用异步方式复制数据到slave节点,不过 Redis 2.8 开始,slave node 会周期性的确认自己每次复制的数据量。
  • 一个 master node 是可以配置多个 slave node 的
  • slave node 做复制的时候,不会block master node 的正常工作。
  • slave node 在做复制的时候,也不会 block 对自己的查询操作,它会用旧的数据集来提供服务;但是复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会暂停对外服务了;

  • slave node 主要用来进行横向扩容,做读写分离,扩容的 slave node 可以提高读的吞吐量。

注意:

  • 如果采用了主从架构,那么建议必须开启 master node 的持久化,不建议用 slave node 作为 master node 的数据热备,因为那样的话,如果你关掉 master 的持久化,可能在 master 宕机重启的时候数据是空的,然后可能一经过复制, slave node 的数据也丢了。

  • 另外,master 的各种备份方案,也需要做。万一本地的所有文件丢失了,从备份中挑选一份 rdb 去恢复 master,这样才能确保启动的时候,是有数据的,即使采用了后续讲解的高可用机制,slave node 可以自动接管 master node,但也可能 sentinel 还没检测到 master failure,master node 就自动重启了,还是可能导致上面所有的 slave node 数据被清空。

5.2 Redis 主从复制的核心原理

  • 当启动一个 slave node 的时候,它会发送一个 PSYNC 命令给 master node。

  • 如果这是 slave node 初次连接到 master node,那么会触发一次 full resynchronization 全量复制。此时 master 会启动一个后台线程,开始生成一份 RDB 快照文件,

  • 同时还会将从客户端 client 新收到的所有写命令缓存在内存中。RDB 文件生成完毕后, master 会将这个 RDB 发送给 slave,slave 会先写入本地磁盘,然后再从本地磁盘加载到内存中,

  • 接着 master 会将内存中缓存的写命令发送到 slave,slave 也会同步这些数据。

  • slave node 如果跟 master node 有网络故障,断开了连接,会自动重连,连接之后 master node 仅会复制给 slave 部分缺少的数据。

过程原理:

  1. 当从库和主库建立MS关系后,会向主数据库发送SYNC命令

  2. 主库接收到SYNC命令后会开始在后台保存快照(RDB持久化过程),并将期间接收到的写命令缓存起来

  3. 当快照完成后,主Redis会将快照文件和所有缓存的写命令发送给从Redis

  4. 从Redis接收到后,会载入快照文件并且执行收到的缓存的命令

  5. 之后,主Redis每当接收到写命令时就会将命令发送从Redis,从而保证数据的一致

缺点

所有的slave节点数据的复制和同步都由master节点来处理,会照成master节点压力太大,使用主从从结构来解决

6. Redis集群的主从复制模型是怎么样的?

6.1 Redis Cluster 整体主从模型

Redis 集群采用的是:多主多从 + 每个主节点自带一组从节点的结构。

一句话进行形容:Redis Cluster 的主从复制模型是:多主多从,分片在主,从只备份;主宕机从自动升主,自带故障转移,最终一致性。

整体架构图:

6.1.1 基本结构
  • 整个集群分成多个 master(主节点)
  • 每个 master 负责一部分 hash slot(哈希槽)
  • 每个 master 可以挂 N 个 slave(从节点)
  • slave 只复制对应 master 的数据,不负责槽
  • slave 默认只读,不接受写请求
6.1.2 核心职责
  • master:负责槽、处理读写请求、同步数据给 slave
  • slave:备份数据、分担读请求、master 挂了时自动竞选成为新 master
6.2 集群模式下的主从复制原理

复制过程和普通主从 完全一致,只是多了集群层面的管理:

  1. slave 向 master 发送 PSYNC
  2. 初次:全量复制(RDB + 缓冲区命令)
  3. 正常:增量复制(基于 offset + 复制积压缓冲区)
  4. 持续命令传播:master 异步把写命令发给所有 slave

特点:

  • 依然是 异步复制
  • 依然存在主从延迟
  • 集群不保证强一致,是最终一致

主从细节图:

6.3 集群模式主从的关键特性(重点)

1. 每个槽只属于一个主节点

  • 数据分片是在 master 之间
  • slave 不参与分片,只做备份和高可用

2. 主从关系是固定绑定的

  • slave 只复制自己对应的 master
  • 不会跨 master 复制
  • 集群通过 Gossip 协议维护主从关系

3. 自动故障转移(集群自带,不需要哨兵)

当一个 master 被半数以上节点认为宕机:

  1. 它的所有 slave 会发起选举
  2. 其中一个 slave 升级为 新 master
  3. 接管原 master 的所有槽
  4. 其他 slave 改为复制新 master
  5. 原 master 重启后会变成新 master 的 slave

这就是 Redis 集群 自带高可用 的原因。

4. 读写分离依然可用

  • 写请求:只能进 master
  • 读请求:可以走 master,也可以走 slave
  • 客户端可以实现读从、写主,提高读并发

但要注意:读 slave 可能读到旧数据(因为异步复制)。

故障转移模型(主节点挂了)

7. Redis 哈希槽(Hash Slot)的概念

Redis 集群用 16384 个固定的哈希槽,来实现数据分片,让 key 均匀分布到不同主节点上。

一句话进行形容:Redis 集群将数据划分为 16384 个固定哈希槽,通过 CRC16 (key)&16383 计算 key 所属槽位,再将槽分配给不同主节点,实现数据分片、水平扩容与便捷扩缩容。

7.1 哈希槽是什么?
  • Redis Cluster 固定划分为 16384 个哈希槽,编号:0 ~ 16383
  • 这些槽是虚拟的逻辑分片单位
  • 整个集群的数据,就分散在这 16384 个槽里
  • 每个主节点负责其中一部分槽

简单理解:16384 个小抽屉 → 分给多个主节点 → 每个 key 进一个抽屉

7.2 为什么是 16384?
  • 16384 = 2¹⁴,刚好是 16k
  • 不大不小,既保证分布均匀,又不至于节点元数据太大
  • 官方设计就是固定死的,不能改
7.3 key 怎么落到哈希槽?(核心公式)
slot = CRC16(key) & 16383

步骤:

  1. 对 key 做 CRC16 哈希
  2. 和 16383 做按位与运算(等价于对 16384 取模)
  3. 得到 0~16383 中的一个槽号
  4. 客户端根据槽 → 节点的映射,找到对应节点
7.4 槽与节点的关系
  • 一个槽 只属于一个主节点
  • 一个主节点可以负责 多个槽
  • 从节点不负责槽,只复制主节点的槽数据

示例(3 主集群):

  • Master1:槽 0 ~ 5000
  • Master2:槽 5001 ~ 10000
  • Master3:槽 10001 ~ 16383
7.5 哈希槽解决了什么问题?
  1. 均匀分片key 均匀散列到不同节点,避免数据倾斜

  2. 扩缩容非常方便加节点 = 迁移一部分槽减节点 = 把槽移走key 与槽的映射关系不变,只变槽与节点的关系

  3. 避免一致性哈希的缺点没有数据倾斜、不用虚拟节点、运维更简单

  4. 支持批量操作(多 key)只要多个 key 在同一个槽,就可以执行 mget、mset 等用 {hash_tag} 强制同槽:

    user:{100}:name
    user:{100}:age
    
7.6 槽与集群高可用
  • 主节点挂了 → 从节点升级为主
  • 槽会整体被新主接管,不需要重新计算
  • 集群对外依然完整可用

8. Redis集群会有写操作丢失吗?为什么?

8.1 结论先说

会丢失写操作。Redis 集群是最终一致性,不保证强一致,异步复制 + 网络分区都可能导致写丢失。

        

Redis 集群会发生写操作丢失,主要原因有两点:

  1. 主从复制是异步的Master 执行写后立即返回客户端成功,之后才异步同步到 Slave。如果 Master 同步前宕机,这部分数据就会丢失。

  2. 网络分区可能导致脑裂某个 Master 被网络隔离,仍能接收写入;集群其他节点已选举新 Master 并提供服务;网络恢复后,旧 Master 被降级为从库,隔离期间的写入全部丢失。

因此 Redis 集群是最终一致性,不保证强一致,属于 AP 系统。

8.2 为什么会丢写?(核心两个原因)

① 主从复制是 异步 的(最主要原因)

  1. 客户端向 Master 发送写命令
  2. Master 执行成功,立即返回 OK 给客户端
  3. 之后异步把命令同步给 Slave

如果:Master 刚返回成功,但还没同步到 Slave 就宕机了→ 这部分写就永久丢失

② 网络分区 + 脑裂场景(典型丢写)

这是 Redis 集群最经典的写丢失场景

  1. 集群出现网络分区

    • 一部分节点在 A 分区
    • 某一个 Master 被隔离到 B 分区(少数派)
  2. 客户端还能访问这个孤立 Master,继续写数据

  3. meanwhile,A 分区内的其他节点认为这个 Master 挂了→ 选举它的 Slave 成为新 Master

  4. 网络恢复,旧 Master 被拉回集群→ 被降级为 Slave,同步新 Master 数据→ 它在隔离期间收到的所有写全部丢失

这就是典型的脑裂导致写丢失

8.3 官方为什么这么设计?

因为 Redis 追求高性能 + 可用性,在 CAP 里选择了 AP,放弃强一致性 C。

  • 如果要强一致 → 必须同步复制、等所有从库确认
  • 性能会大幅下降,失去 Redis 高并发优势

所以官方明确:Redis Cluster does not guarantee strong consistency.

8.4 Redis 减少写丢失的核心方案:WAIT 命令

Redis 集群无法完全避免写丢失,但可以通过以下方式降低风险:

  1. 使用 WAIT 命令同步等待指定数量从库复制完成,保证写至少落到一主一从,减少单机宕机导致的数据丢失。

  2. 配置 min-replicas-to-write确保主节点只有在足够从库在线时才接受写入,避免无备份情况下写入。

但由于 Redis 是 AP 系统,主从异步复制、网络分区脑裂等问题无法彻底解决,因此无法实现强一致,仍存在写丢失可能

        

Redis 从 3.0+ 开始提供 WAIT 命令,用来实现同步复制确认,强制主节点等待从节点同步完成再返回。

命令格式

WAIT  <需要同步的从库数量>  <超时时间(毫秒)>

示例:

SET key value
WAIT 1 500

含义:

  • 等待 至少 1 个从库 同步完成
  • 最多等 500ms,超时就不等了
  • 命令返回:实际同步成功的从库数量
8.5 WAIT 原理
  1. 主节点执行完写命令,不立即返回
  2. 等待指定数量的从库复制偏移量追上
  3. 达到数量 or 超时 → 返回客户端

它实现的是:弱同步复制 → 尽可能保证数据不丢

8.6  WAIT 能解决什么?
  1. 解决 “主刚写完就挂,数据没同步到从” 的问题
  2. 大幅降低写丢失概率
  3. 适合对数据可靠性要求高的场景:订单、余额、重要状态
8.7 WAIT 不能解决什么?(重点)

不能 100% 避免写丢失!不能解决 网络分区脑裂 问题。

举例:主被网络隔离 → 还能接收 WAIT 写 → 但集群另一边已经选举新主→ 网络恢复后,旧主的数据依然会丢失。

因为 WAIT 只保证复制到从,不保证集群全局一致性

8.8 Redis 还有一种配置:min-replicas-to-write

redis.conf 里:

min-replicas-to-write 1
min-replicas-max-lag 10

含义:

  • 主节点至少要有 1 个从库在线且延迟 <10s
  • 否则主节点拒绝写入,直接报错

作用:

  • 避免主在 “没有可用从库” 的情况下还疯狂写
  • 减少写丢失风险

缺点:

  • 可用性下降,从库挂了主也不能写
  • 依然不能解决脑裂
8.9 总结:能不能完全不丢写?

结论:

Redis 集群无论如何配置,都无法做到绝对不丢写。

原因:

  1. Redis 是 AP 系统,设计上就不追求强一致性
  2. 网络分区(脑裂)是分布式系统固有问题,Redis 无法规避
  3. 即使 WAIT + min-replicas 也只能降低概率,不能根除

9. Redis 分区

9.1 Redis 是单线程地,如何提高多核 CPU 的利用率?

可以在同一个服务器部署多个Redis的实例,并把它们当作不同的服务器来进行使用,在某些时候,无论如何一个服务器是不够的,所以如果想使用多个CPU,可以考虑分片(shard)。

9.2 为什么要做Redis分区?

分区可以让Redis 管理更大的内存,Redis 将可以使用所有机器的内存。如果没有分区,最好只能使用一台机器的内存。分区使 Redis 的计算能力通过简单地增加计算机得到成倍提升,Redis 的网络带宽也会随着计算机和网卡的增加而成倍增加。

9.3 Redis 分区实现方案

1. 客户端分区就是在客户端就已经决定数据会被存储到哪个Redis节点或者从哪个Redis 节点读取。大多数客户端已经实现了客户端分区。

2. 代理分区 意味着客户端将请求发送到代理,然后代理决定去哪个节点写数据或者读数据。代理根据分区规则决定请求哪些是Redis实例,然后根据 Redis 的响应结果返回给客户端。Redis和memcached 的一种代理实现就是 Twemproxy。

3. 查询路由(Query routing)的意思是客户端随机地请求任意一个Redis实例,然后由Redis 将请求转发给正确的Redis节点。Redis Cluster 实现了一种混合形式的查询路由,但并不是直接将请求从一个Redis 节点转发到另一个 Redis 节点,而是在客户端的帮助下直接转发到正确的Redis 节点。

9.4 Redis 分区有什么缺点?

1. 涉及多个key 的操作通常不会被支持。例如你不能对两个集合就交集,因为它们可能被存储到不用的Redis 实例中(实际上这种情况也有办法,但是不能直接使用交集指令)。

2. 同时操作多个key,则不能使用Redis事务。

3. 分区使用的粒度是key,不能使用一个非常长的排序Key 存储一个数据库

4. 当使用分区的时候,数据处理会非常复杂,例如为了备份必须从不同的Redis实例和主机同时收集 RDB/AOF 文件。

5. 分区时动态扩容或缩容可能非常复杂。Redis 集群在运行时增加或者删除Redis节点,能做到最大程度对用户透明地数据再平衡,但其他一些客户端分区或者代理分区方法不支持这种特性。然而,有一种预分片的技术也可以较好的解决这个问题。

10  分布式问题

10.1 Redis实现分布式锁

  • Redis 为单进程单线程模式,采用队列模式将并发访问编程串行访问,且多客户端对Redis 的连接并不存在竞争关系,Redis中可以使用setNx命令实现分布式锁。
  • 当且仅当key不存在,将key 的值设为value。若给定的key已经存在,则setNx不做任何动作。
  • SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。

  • 返回值:设置成功,返回 1 。设置失败,返回 0 。

使用setNx完成同步锁的流程及事项如下:

  • 使用SETNX命令获取锁,若返回0(key已存在,锁已存在)则获取失败,反之获取成功

  • 为了防止获取锁后程序出现异常,导致其他线程/进程调用setNx命令总是返回0而进入死锁状态,需要为该key设置一个“合理”的过期时间释放锁,使用DEL命令将锁数据删除

10.2 如何解决Redis 的并发竞争 key问题

  • 所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同!

  • 推荐一种方案:分布式锁(zookeeper 和 redis 都可以实现分布式锁)。(如果不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能)

  • 基于zookeeper临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。

在实践中,当然是从以可靠性为主。所以首推Zookeeper。

10.3 分布式Redis是前期做还是后期规模上来了再做好?为什么?

  • 既然Redis是如此的轻量(单实例只使用1M内存),为防止以后的扩容,最好的办法就是一开始就启动较多实例。即便你只有一台服务器,你也可以一开始就让Redis以分布式的方式运行,使用分区,在同一台服务器上启动多个实例。

  • 一开始就多设置几个Redis实例,例如32或者64个实例,对大多数用户来说这操作起来可能比较麻烦,但是从长久来看做这点牺牲是值得的。

  • 这样的话,当你的数据不断增长,需要更多的Redis服务器时,你需要做的就是仅仅将Redis实例从一台服务迁移到另外一台服务器而已(而不用考虑重新分区的问题)。一旦你添加了另一台服务器,你需要将你一半的Redis实例从第一台机器迁移到第二台机器。

10.4 什么是 RedLock

Redis 官方站提出了一种权威的基于 Redis 实现分布式锁的方式名叫Redlock,此种方式比原先的单节点的方法更安全。它可以保证以下特性:

  • 安全特性:互斥访问,即永远只有一个 client 能拿到锁
  • 避免死锁:最终 client 都可能拿到锁,不会出现死锁的情况,即使原本锁住某资源的 client crash 了或者出现了网络分区
  • 容错性:只要大部分 Redis 节点存活就可以正常提供服务

11. 看门狗机制

11.1 什么是看门狗机制?

redission看门狗机制是解决分布式锁的续约问题。
Redisson提供的分布式锁是支持锁自动续期的,也就是说,如果线程仍旧没有执行完,那么redisson会自动给redis中的目标key延长超时时间,这在Redisson中称之为 Watch Dog 机制。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。

RLock lock = redissonClient.getLock("guodong");    // 拿锁失败时会不停的重试
// 具有Watch Dog 自动延期机制 默认续30s 每隔30/3=10 秒续到30s
lock.lock();
// 尝试拿锁10s后,没有Watch Dog
lock.lock(10, TimeUnit.SECONDS);
// 尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
// 尝试拿锁10s后停止重试,返回false 具有Watch Dog 自动延期机制 默认续30s
boolean res1 = lock.tryLock(10, TimeUnit.SECONDS); 
// 没有Watch Dog ,10s后自动释放
lock.lock(10, TimeUnit.SECONDS);
// 尝试拿锁100s后停止重试,返回false 没有Watch Dog ,10s后自动释放
boolean res2 = lock.tryLock(100, 10, TimeUnit.SECONDS);

11.2 源码解读

注意

watchDog 只有在未显示指定锁自动释放时间(leaseTime)时才会生效。(这点很重要)
lockWatchdogTimeout设定的时间不要太小 ,比如我之前设置的是 100毫秒,由于网络直接导致加锁完后,watchdog去延期时,这个key在redis中已经被删除了。

再调用Lock 方法时,会最终调用tryAcquireAsync。调用链为:lock()->tryAcquire->tryAcquireAsync,详细解释如下:

private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
        RFuture<Long> ttlRemainingFuture;
        //如果指定了加锁时间,会直接去加锁
        if (leaseTime != -1) {
            ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        } else {
            //没有指定加锁时间 会先进行加锁,并且默认时间就是 LockWatchdogTimeout的时间
            //这个是异步操作 返回RFuture 类似netty中的future
            ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
                    TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
        }
 
        //这里也是类似netty Future 的addListener,在future内容执行完成后执行
        ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
            if (e != null) {
                return;
            }
 
            // lock acquired
            if (ttlRemaining == null) {
                // leaseTime不为-1时,不会自动延期
                if (leaseTime != -1) {
                    internalLockLeaseTime = unit.toMillis(leaseTime);
                } else {
                    //这里是定时执行 当前锁自动延期的动作,leaseTime为-1时,才会自动延期
                    scheduleExpirationRenewal(threadId);
                }
            }
        });
        return ttlRemainingFuture;
    }

ttlRemaining==null,这个ttlRemaining也就是加锁成功的返回的null值

scheduleExpirationRenewal 中会调用renewExpiration。 这里我们可以看到是,启用了一个timeout定时,去执行延期

private void renewExpiration() {
        ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
        if (ee == null) {
            return;
        }
 
        //这是一个定时任务。
        Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {
                ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
                if (ent == null) {
                    return;
                }
                Long threadId = ent.getFirstThreadId();
                if (threadId == null) {
                    return;
                }
 
                //这任务需要执行的核心代码。
                RFuture<Boolean> future = renewExpirationAsync(threadId);
                future.onComplete((res, e) -> {
                    if (e != null) {
                        log.error("Can't update lock " + getRawName() + " expiration", e);
                        EXPIRATION_RENEWAL_MAP.remove(getEntryName());
                        return;
                    }
 
                    if (res) {
                        //如果 没有报错,就再次定时延期
                        // reschedule itself
                        renewExpiration();
                    } else {
                        cancelExpirationRenewal(null);
                    }
                });
            }
            
            // 这里我们可以看到定时任务 是 lockWatchdogTimeout 的1/3时间去执行而 internalLockLeaseTime 默认为 30000。所以该任务每 10s 执行一次,renewExpirationAsync
        }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
 
        ee.setTimeout(task);
    }

最终 scheduleExpirationRenewal会调用到 renewExpirationAsync,执行下面这段 lua脚本。他主要判断就是 这个锁是否在redis中存在,如果存在就进行 pexpire 延期.

protected RFuture<Boolean> renewExpirationAsync(long threadId) {
        return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                        "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                        "return 1; " +
                        "end; " +
                        "return 0;",
                Collections.singletonList(getRawName()),
                internalLockLeaseTime, getLockName(threadId));
    }

所以,每当 key 的 ttl(剩余时间)为 20 的时候,则进行续命操作,重新将 key 的过期时间设置为默认时间 30s。

11.3 结论

watch dog 在当前节点存活时每10s给分布式锁的key续期 30s;
watch dog 机制启动,且代码中没有释放锁操作时,watch dog 会不断的给锁续期;
如果程序释放锁操作时因为异常没有被执行,那么锁无法被释放,所以释放锁操作一定要放到 finally {} 中;
要使 watchLog机制生效 ,lock时 不要设置 过期时间
watchlog的延时时间 可以由 lockWatchdogTimeout指定默认延时时间,但是不要设置太小。如100
watchdog 会每 lockWatchdogTimeout/3时间,去延时。
watchdog 通过 类似netty的 Future功能来实现异步延时
watchdog 最终还是通过 lua脚本来进行延时。

11.4 设置了过期时间,如果业务还没有执行完成,但是redis锁过期了,怎么办?

设置锁成功后,启动一个watchdog,每隔一段时间(默认10s)为当前分布式锁续约。

12 缓存异常

12.1 缓存预热

  • 缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!

  • 解决方案

    1. 直接写个缓存刷新页面,上线时手工操作一下;

    2. 数据量不大,可以在项目启动的时候自动进行加载;

    3. 定时刷新缓存;

12.2 缓存降级

  • 当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。

  • 缓存降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。

  • 在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:

    1. 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;

    2. 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;

    3. 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;

    4. 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。

  • 服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。

12.3 热点数据和冷数据

  • 热点数据,缓存才有价值

  • 对于冷数据而言,大部分数据可能还没有再次访问到就已经被挤出内存,不仅占用内存,而且价值不大。频繁修改的数据,看情况考虑使用缓存

  • 对于热点数据,比如我们的某IM产品,生日祝福模块,当天的寿星列表,缓存以后可能读取数十万次。再举个例子,某导航产品,我们将导航信息,缓存以后可能读取数百万次。

  • 数据更新前至少读取两次,缓存才有意义。这个是最基本的策略,如果缓存还没有起作用就失效了,那就没有太大价值了。

  • 那存不存在,修改频率很高,但是又不得不考虑缓存的场景呢?有!比如,这个读取接口对数据库的压力很大,但是又是热点数据,这个时候就需要考虑通过缓存手段,减少数据库的压力,比如我们的某助手产品的,点赞数,收藏数,分享数等是非常典型的热点数据,但是又不断变化,此时就需要将数据同步保存到Redis缓存,减少数据库压力。

12.4  缓存热点key

缓存中的一个Key(比如一个促销商品),在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

解决方案

对缓存查询加锁,如果KEY不存在,就加锁,然后查DB入缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入DB查询

13 其他问题

13.1  Redis与Memcached的区别

  • 两者都是非关系型内存键值数据库,现在公司一般都是用 Redis 来实现缓存,而且 Redis 自身也越来越强大了!Redis 与 Memcached 主要有以下不同:

对比参数RedisMemcached
类型1. 支持内存 2. 非关系型数据库1. 支持内存 2. 键值对形式 3. 缓存形式
数据存储类型1. String 2. List 3. Set 4. Hash 5. Sort Set 【俗称ZSet】1. 文本型 2. 二进制类型
查询【操作】类型1. 批量操作 2. 事务支持 3. 每个类型不同的CRUD1.常用的CRUD 2. 少量的其他命令
附加功能1. 发布/订阅模式 2. 主从分区 3. 序列化支持 4. 脚本支持【Lua脚本】1. 多线程服务支持
网络IO模型1. 单线程的多路 IO 复用模型1. 多线程,非阻塞IO模式
事件库自封转简易事件库AeEvent贵族血统的LibEvent事件库
持久化支持1. RDB 2. AOF不支持
集群模式原生支持 cluster 模式,可以实现主从复制,读写分离没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据
内存管理机制在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘Memcached 的数据则会一直在内存中,Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题。但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。
适用场景复杂数据结构,有持久化,高可用需求,value存储内容较大纯key-value,数据量非常大,并发量非常大的业务
  1. memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据类型

  2. redis的速度比memcached快很多

  3. redis可以持久化其数据

13.2 如何保证缓存与数据库双写时的数据一致性?

  • 你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?

  • 一般来说,就是如果你的系统不是严格要求缓存+数据库必须一致性的话,缓存可以稍微的跟数据库偶尔有不一致的情况,最好不要做这个方案,读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况

  • 串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。

  • 还有一种方式就是可能会暂时产生不一致的情况,但是发生的几率特别小,就是先更新数据库,然后再删除缓存。

问题场景描述解决
先写缓存,再写数据库,缓存写成功,数据库写失败缓存写成功,但写数据库失败或者响应延迟,则下次读取(并发读)缓存时,就出现脏读这个写缓存的方式,本身就是错误的,需要改为先写数据库,把旧缓存置为失效;读取数据的时候,如果缓存不存在,则读取数据库再写缓存
先写数据库,再写缓存,数据库写成功,缓存写失败写数据库成功,但写缓存失败,则下次读取(并发读)缓存时,则读不到数据缓存使用时,假如读缓存失败,先读数据库,再回写缓存的方式实现
需要缓存异步刷新指数据库操作和写缓存不在一个操作步骤中,比如在分布式场景下,无法做到同时写缓存或需要异步刷新(补救措施)时候确定哪些数据适合此类场景,根据经验值确定合理的数据不一致时间,用户数据刷新的时间间隔

13.3 Redis常见性能问题和解决方案?

  1. Master最好不要做任何持久化工作,包括内存快照和AOF日志文件,特别是不要启用内存快照做持久化。

  2. 如果数据比较关键,某个Slave开启AOF备份数据,策略为每秒同步一次。

  3. 为了主从复制的速度和连接的稳定性,Slave和Master最好在同一个局域网内。

  4. 尽量避免在压力较大的主库上增加从库

  5. Master调用BGREWRITEAOF重写AOF文件,AOF在重写的时候会占大量的CPU和内存资源,导致服务load过高,出现短暂服务暂停现象。

  6. 为了Master的稳定性,主从复制不要用图状结构,用单向链表结构更稳定,即主从关系为:Master<–Slave1<–Slave2<–Slave3…,这样的结构也方便解决单点故障问题,实现Slave对Master的替换,也即,如果Master挂了,可以立马启用Slave1做Master,其他不变。

13.4 Redis官方为什么不提供Windows版本?

因为目前Linux版本已经相当稳定,而且用户量很大,无需开发windows版本,反而会带来兼容性等问题。

13.5  一个字符串类型的值能存储最大容量是多少?

512M

13.6  Redis如何做大量数据插入?

Redis2.6开始redis-cli支持一种新的被称之为pipe mode的新模式用于执行大量数据插入工作。

13.7 假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来?

  • 使用keys指令可以扫出指定模式的key列表。

  • 对方接着追问:如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题? 这个时候你要回答redis关键的一个特性:redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。

13.8 使用Redis做过异步队列吗,是如何实现的

使用list类型保存数据信息,rpush生产消息,lpop消费消息,当lpop没有消息时,可以sleep一段时间,然后再检查有没有信息,如果不想sleep的话,可以使用blpop, 在没有信息的时候,会一直阻塞,直到信息的到来。redis可以通过pub/sub主题订阅模式实现一个生产者,多个消费者,当然也存在一定的缺点,当消费者下线时,生产的消息会丢失。

13.9  Redis如何实现延时队列

使用sortedset,使用时间戳做score, 消息内容作为key,调用zadd来生产消息,消费者使用zrangbyscore获取n秒之前的数据做轮询处理。

13.10  Redis回收进程如何工作的?

  1. 一个客户端运行了新的命令,添加了新的数据。

  2. Redis检查内存使用情况,如果大于maxmemory的限制, 则根据设定好的策略进行回收。

  3. 一个新的命令被执行,等等。

  4. 所以我们不断地穿越内存限制的边界,通过不断达到边界然后不断地回收回到边界以下。

如果一个命令的结果导致大量内存被使用(例如很大的集合的交集保存到一个新的键),不用多久内存限制就会被这个内存使用量超越。
13.11  Redis回收使用的是什么算法?

LRU算法

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值