REmote DIctionary Server(Redis) 是一个由Salvatore Sanfilippo写的key-value存储系统,是垮平台的非关系型数据库

redis是一个开源的,使用ANSIC语言编写、遵守BSD协议、支持网络、可基于内存、分布式、可选持久性的键值对存储数据库,并提供多种语言的API

redis通常被称为数据结构服务器,因为其值可以是字符串、哈希、列表、集合和有序集合等类型。

redis的数据类型以及使用场景

String

这个没啥好说的,最常规的 set/get 操作,Value 可以是 String 也可以是数字。一般做一些复杂的计数功能的缓存。键和值最大能存储512M

String类型是二进制安全的。意思是redis的string可以包含任何数据,比如jpg图片或者序列化的对象

  1. redis 127.0.0.1:6379> SET runoob "菜鸟教程"
  2. OK
  3. redis 127.0.0.1:6379> GET runoob
  4. "菜鸟教程"

Hash

可以有多个键值对,每个hash可以存储2^32-1个键值对,即40多亿个

这里 Value 存放的是结构化的对象,比较方便的就是操作其中的某个字段。

我在做单点登录的时候,就是用这种数据结构存储用户信息,以 CookieId 作为 Key,设置 30 分钟为缓存过期时间,能很好的模拟出类似 Session 的效果。

  1. redis 127.0.0.1:6379> HMSET runoob field1 "Hello" field2 "World"
  2. "OK"
  3. redis 127.0.0.1:6379> HGET runoob field1
  4. "Hello"
  5. redis 127.0.0.1:6379> HGET runoob field2
  6. "World"

List

使用 List 的数据结构,可以做简单的消息队列的功能。另外还有一个就是,可以利用 lrange 命令,做基于 Redis 的分页功能,性能极佳,用户体验好。

  1. redis 127.0.0.1:6379> lpush runoob redis
  2. (integer) 1
  3. redis 127.0.0.1:6379> lpush runoob mongodb
  4. (integer) 2
  5. redis 127.0.0.1:6379> lpush runoob rabbitmq
  6. (integer) 3
  7. redis 127.0.0.1:6379> lrange runoob 0 10
  8. 1) "rabbitmq"
  9. 2) "mongodb"
  10. 3) "redis"

列表最多可存储2^32-1个元素,即40多亿

Set

因为 Set 堆放的是一堆不重复值的集合。所以可以做全局去重的功能。为什么不用 JVM 自带的 Set 进行去重?

因为我们的系统一般都是集群部署,使用 JVM 自带的 Set,比较麻烦,难道为了一个做一个全局去重,再起一个公共服务,太麻烦了。

另外,就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。

  1. redis 127.0.0.1:6379> DEL runoob
  2. redis 127.0.0.1:6379> sadd runoob redis
  3. (integer) 1
  4. redis 127.0.0.1:6379> sadd runoob mongodb
  5. (integer) 1
  6. redis 127.0.0.1:6379> sadd runoob rabbitmq
  7. (integer) 1
  8. redis 127.0.0.1:6379> sadd runoob rabbitmq
  9. (integer) 0
  10. redis 127.0.0.1:6379> smembers runoob
  11. 1) "redis"
  12. 2) "rabbitmq"
  13. 3) "mongodb"

sadd,成功返回1,不成功返回0

集合中最大的成员数为 232 - 1(4294967295, 每个集合可存储40多亿个成员)。

Sorted Set(Zset)

Sorted Set多了一个权重参数 Score,集合中的元素能够按 Score 进行排列。

可以做排行榜应用,取 TOP N 操作。Sorted Set 可以用来做延时任务。最后一个应用就是可以做范围查找。

  1. redis 127.0.0.1:6379> zadd runoob 0 redis
  2. (integer) 1
  3. redis 127.0.0.1:6379> zadd runoob 0 mongodb
  4. (integer) 1
  5. redis 127.0.0.1:6379> zadd runoob 0 rabbitmq
  6. (integer) 1
  7. redis 127.0.0.1:6379> zadd runoob 0 rabbitmq
  8. (integer) 0
  9. redis 127.0.0.1:6379> ZRANGEBYSCORE runoob 0 1000
  10. 1) "mongodb"
  11. 2) "rabbitmq"
  12. 3) "redis"

redis缓存

基于内存的键值对库,搜索速度是O(1)级别的

可以做二级缓存,最有效地改进项目性能,可以做数据的临时存储,可以设置过期时间

为什么要做缓存?是为了更快速的查询

一般数据查询都是从数据库查询的,当并发量大的时候数据库压力就会大,怎么解决?使用缓存提升数据性能

使用redis缓存后,首先会到redis中进行查找,如果redis中没有,redis则会从数据库中查找,把查到的数据存到redis中,再返回到客户端,以后每次查找这个数据都会从redis中查找,降低了数据库的压力,提高了性能,且从数据库查数据会比redis慢得多

为什么从数据库查数据会慢的多?当使用数据库存的时候,底层时会用数据库引擎进行存储,例如innoDb,而innoDb底层使用了B+树(平衡树),在该树查数据要经过根节点,枝节点,叶节点,在根据叶节点去定位块,从块上面搜数据

缓存预热

一开始缓存中没数据,当并发量大的时候,所有请求对被定位到数据库中,为了解决这个问题用到了缓存预热机制。

当系统一开启的时候,将数据加载到缓存系统中,加载方式:1.系统启动时加载 2.定时加载

加载考虑的是热点数据,另启一个线程,统计数据库里的数据在单位时间内被命中的次数,抽取出来,可以放到kafka中,总kafka中读到redis中

pipeline(管道)

并不是事务,而是redis中的一个方法

优点:在没用pipeline之前,客户端每发送一个命令,redis经过计算后返回一个结果,是一个阻塞式发的顺序操作,redis没回传结果的时候回会一直阻塞,直到返回结果后客户端才能发送下一个命令

为了防止网络抖动,pipeline机制把多个操作合成一个整体,把多个命令当成一个整体发送给redis,一起执行,然后返回N个结果,提升网络性能

持久化

redis是一个基于内存的库,而内存的库最怕的是内存所在的主机突然宕机,数据就会丢失

redis提供了两种持久化机制

(1)、RDB(redis database),能把内存中的数据持久化到磁盘的一种方案,存磁盘的镜像:在指定的时间间隔内将内存中的数据集一次性第dump(底层操作系统的命令)到一个RDB的临时文件,然后覆盖原来磁盘上的RDB文件(二进制文件,即镜像文件)

什么时候触发:
1)调用save指令,同步方案
2)调用bgsave指令,后台异步操作,保存镜像的同时还能响应客户端的请求
3)自动触发,通过配置实现

save与bgsave的区别:

命令 save bgsave
IO类型 同步 异步
阻塞 是(阻塞发生在fork)
复杂度 O(n) O(n)
优点 不会消耗额外的内存 不阻塞客户端命令
缺点 阻塞客户端命令 需要fork,消耗内存

RDB的优势:
1.RDB文件紧凑,RDB以二进制文件存储,可以压缩,非常适合全量备份(把整个内存的数据一次性备份);

2.生成RDB文件时,redis主进程会fork()(fork是unix指令,用来产生新的进程)一个子进程来处理所有保存工作,这样主进程不用做任何的保存工作;

3.RDB在恢复大数据集时的速度比AOF快,因为RDB相当于一个镜像,而AOF存的是一条条的指令

劣势:RDB由于会开启子进程,且该子进程会拥有父进程的内存数据,当父进程修改内存时,子进程不会反应出来,所以在快照持久化期间修改的数据不会被保存,这就会导致数据的丢失。

(2)、AOF(append of file)追加文件,里面存的是操作的指令,客户端进行的一系列操作都会存储在一个AOF文件中,该文件是可见的且会越来越大。

  1. 为了压缩AOF文件,rediss提供了bgrewriteaof的命令,用来重写AOF文件,将内存中的数据以命令的方式保存到临时文件中,同时会fork出一条新进程来将文件重写。重写AOF文件的操作,并没有读取旧的AOF文件,而是将整个内存中的数据内容用命令的方式重写了一个新的AOF文件,和快照有点类似

触发机制:
1)修改同步(always),当对redis库进行增删改查的时候,记录该操作

2)每秒同步(everysec),一秒中记录一次

3)不同,即no

命令 always everysec no
优点 不丢失数据 每秒一次fsync丢失一秒数据 不用管
缺点 IO开销大,一般的sata盘只有几百TPS 丢失一秒数据 不可控

AOF的优点:1.AOF可以更好的保护数据不丢失,最多丢失一秒的数据

  1. 2.AOF日志文件没有任何磁盘寻址的开销,写入性很高,文件不易损坏
  2. 3.AOF文件即使在重写的时候也不会影响客户端的读写
  3. 4.可做灾难性的误删除的紧急恢复
  4. 缺点:1.对于同一份数据来说,AOF文件通常比RDB
  5. 2.QPSRDB
  6. 3.发生过bug,在进行数据恢复时不一致

redis发布订阅

redis发布订阅是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息

redis客户端可以订阅任意数量的频道

image.png

实例

创建订阅频道名为runoobChat

第一个redis-cli客户端

  1. redis 127.0.0.1:6379> SUBSCRIBE runoobChat #订阅runoobChat频道
  2. Reading messages... (press Ctrl-C to quit)
  3. 1) "subscribe"
  4. 2) "runoobChat"
  5. 3) (integer) 1

开启第二个redis-cli客户端

  1. redis 127.0.0.1:6379> PUBLISH runoobChat "Redis PUBLISH test" #往runoobChat发送消息
  2. (integer) 1
  3. redis 127.0.0.1:6379> PUBLISH runoobChat "Learn redis by runoob.com"
  4. (integer) 1
  5. # 订阅者的客户端会显示如下消息
  6. 1) "message"
  7. 2) "runoobChat"
  8. 3) "Redis PUBLISH test"
  9. 1) "message"
  10. 2) "runoobChat"
  11. 3) "Learn redis by runoob.com"

redis事务

redis事务可以一次执行过个命令,并且带有以下三个重要的保证:

  • 批量操作在发送EXEC命令钱被放入队列缓存
  • 收到EXEC命令后进入事务执行,事务中人体命令执行失败,其余的命令依然被执行
  • 在事务执行过程,其他客户端提交的命令请求不会插入到事务中型命令序列中

以MULTI开始一个事务,然后将多个命令入队到事务中,最后由EXEC命令触发事务,一并执行事务中的所有命令

  1. redis 127.0.0.1:6379> MULTI
  2. OK
  3. redis 127.0.0.1:6379> SET book-name "Mastering C++ in 21 days"
  4. QUEUED
  5. redis 127.0.0.1:6379> GET book-name
  6. QUEUED
  7. redis 127.0.0.1:6379> SADD tag "C++" "Programming" "Mastering Series"
  8. QUEUED
  9. redis 127.0.0.1:6379> SMEMBERS tag
  10. QUEUED
  11. redis 127.0.0.1:6379> EXEC
  12. 1) OK
  13. 2) "Mastering C++ in 21 days"
  14. 3) (integer) 3
  15. 4) 1) "Mastering Series"
  16. 2) "C++"
  17. 3) "Programming"

单个redis命令的执行是原子性的,但redis没有在事务上增加任何维持原子性的机制,所以redis事务的执行并不是原子性的。

redis事务可以理解为一个打包的批量执行脚本,但批量指令也并不是原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做

Redis脚本

redis脚本使用Lua解释器来执行脚本。Redis2.6版本通过内嵌支持Lua环境,执行脚本的常用命令为EVAL

  1. redis 127.0.0.1:6379> EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
  2. 1) "key1"
  3. 2) "key2"
  4. 3) "first"
  5. 4) "second"

redis分区

分区是分割数据到多个redis实例的处理过程,因此每个实例只保存key的一个子集

优势

通过利用多台计算机内存的和值,允许构造更大的数据库

通过多核和多台计算机,允许扩展计算能力

通过多台计算机和网络适配器,允许扩展网络带宽

不足

涉及多个key的操作通常是不被支持的。举例来说,当两个set映射到不同的redis实例上时,就不能对这两个set执行交集操作

涉及多个key的redis事务不能使用

当使用分区时,数据处理较为复杂,比如需要处理多个rdb或者aof文件,并且从多个实例和主机备份持久化文件

增加或删除容量也比较复杂。redis集群大多数支持在运行时增加、删除节点的透明数据平衡的能力,但是类似于客户端分区、代理等其他系统则不支持这项特性。然而,一种叫做presharding的技术对此是有帮助的

分区类型

范围分区

最简单的分区方式是按范围分区,就是映射一定范围的对象到特定的Redis实例。

比如,ID从0到10000的用户会保存到实例R0,ID从10001到 20000的用户会保存到R1,以此类推。

这种方式是可行的,并且在实际中使用,不足就是要有一个区间范围到实例的映射表。这个表要被管理,同时还需要各 种对象的映射表,通常对Redis来说并非是好的方法。

哈希分区

另外一种分区方法是hash分区。这对任何key都适用,也无需是object_name:这种形式,像下面描述的一样简单:

  • 用一个hash函数将key转换为一个数字,比如使用crc32 hash函数。对key foobar执行crc32(foobar)会输出类似93024922的整数。
  • 对这个整数取模,将其转化为0-3之间的数字,就可以将这个整数映射到4个Redis实例中的一个了。93024922 % 4 = 2,就是说key foobar应该被存到R2实例中。注意:取模操作是取除的余数,通常在多种编程语言中用%操作符实现。

redis分布式锁的原理

为什么要使用分布式锁

关于分布式锁,可能绝大部分人都会或多或少涉及到。
我举二个例子:
场景一:从前端界面发起一笔支付请求,如果前端没有做防重处理,那么可能在某一个时刻会有两笔一样的单子同时到达系统后台。

场景二:在App中下订单的时候,点击确认之后,没反应,就又点击了几次。在这种情况下,如果无法保证该接口的幂等性,那么将会出现重复下单问题。
在接收消息的时候,消息推送重复。如果处理消息的接口无法保证幂等,那么重复消费消息产生的影响可能会非常大。

类似这种场景,我们有很多种方法,可以使用幂等操作,也可以使用锁的操作。
我们先来解释一下什么是幂等操作:
所谓幂等,简单地说,就是对接口的多次调用所产生的结果和调用一次是一致的。扩展一下,这里的接口,可以理解为对外发布的HTTP接口或者Thrift接口,也可以是接收消息的内部接口,甚至是一个内部方法或操作。

在分布式环境中,网络环境更加复杂,
因前端操作抖动、网络故障、消息重复、响应速度慢等原因,对接口的重复调用概率会比集中式环境下更大,尤其是重复消息在分布式环境中很难避免。Tyler Treat也在《You Cannot Have Exactly-Once Delivery》一文中提到:

Within the context of a distributed system, you cannot have exactly-once message delivery.(在分布式系统的上下文中,您不能只进行一次消息传递。)

分布式环境中,有些接口是天然保证幂等性的,如查询操作。有些对数据的修改是一个常量,并且无其他记录和操作,那也可以说是具有幂等性的。其他情况下,所有涉及对数据的修改、状态的变更就都有必要防止重复性操作的发生。通过间接的实现接口的幂等性来防止重复操作所带来的影响,成为了一种有效的解决方案。

redis实现分布式锁

一般在项目中引入Redisson的依赖,然后基于Redis实现分布式锁的加锁与释放锁。

  1. RLock lock = redisson. getLock("myLock" );
  2. lock.lock();
  3. lock.unlock();

此外,Redisson还支持redis单实例、redis哨兵、redis cluster、redis master-slave等各种部署架构,都可以给你完美实现。

用分布式锁如何解决库存超卖问题?

同一个锁key,同一时间只能有一个客户端拿到锁,其他客户端会陷入无限的等待来尝试获取那个锁,只有获取到锁的客户端才能执行下面的业务逻辑。

有没有其他方案可以解决库存超卖问题?

当然有啊!比如悲观锁,分布式锁,乐观锁,队列串行化,异步队列分散,Redis原子操作,等等,很多方案,我们对库存超卖有自己的一整套优化机制。

分布式锁的方案在高并发场景下的问题

分布式锁一旦加了之后,对同一个商品的下单请求,会导致所有客户端都必须对同一个商品的库存锁key进行加锁。

比如,对iphone这个商品的下单,都必对“iphone_stock”这个锁key来加锁。这样会导致对同一个商品的下单请求,就必须串行化,一个接一个的处理。

大家再回去对照上面的图反复看一下,应该能想明白这个问题。

假设加锁之后,释放锁之前,查库存 -> 创建订单 -> 扣减库存,这个过程性能很高吧,算他全过程20毫秒,这应该不错了。

那么1秒是1000毫秒,只能容纳50个对这个商品的请求依次串行完成处理。

比如一秒钟来50个请求,都是对iphone下单的,那么每个请求处理20毫秒,一个一个来,最后1000毫秒正好处理完50个请求。

大家看一眼下面的图,加深一下感觉。

image.png

所以看到这里,大家起码也明白了,简单的使用分布式锁来处理库存超卖问题,存在什么缺陷。

缺陷就是同一个商品多用户同时下单的时候,会基于分布式锁串行化处理,导致没法同时处理同一个商品的大量下单的请求。

这种方案,要是应对那种低并发、无秒杀场景的普通小电商系统,可能还可以接受。

因为如果并发量很低,每秒就不到10个请求,没有瞬时高并发秒杀单个商品的场景的话,其实也很少会对同一个商品在一秒内瞬间下1000个订单,因为小电商系统没那场景。

如何对分布式锁进行高并发优化?

好了,终于引入正题了,那么现在怎么办呢?

面试官说,我现在就卡死,库存超卖就是用分布式锁来解决,而且一秒对一个iphone下上千订单,怎么优化?

现在按照刚才的计算,你一秒钟只能处理针对iphone的50个订单。

其实说出来也很简单,相信很多人看过java里的ConcurrentHashMap的源码和底层原理,应该知道里面的核心思路,就是分段加锁

把数据分成很多个段,每个段是一个单独的锁,所以多个线程过来并发修改数据的时候,可以并发的修改不同段的数据。不至于说,同一时间只能有一个线程独占修改ConcurrentHashMap中的数据。

另外,Java 8中新增了一个LongAdder类,也是针对Java 7以前的AtomicLong进行的优化,解决的是CAS类操作在高并发场景下,使用乐观锁思路,会导致大量线程长时间重复循环。

LongAdder中也是采用了类似的分段CAS操作,失败则自动迁移到下一个分段进行CAS的思路。

其实分布式锁的优化思路也是类似的,之前我们是在另外一个业务场景下落地了这个方案到生产中,不是在库存超卖问题里用的。

但是库存超卖这个业务场景不错,很容易理解,所以我们就用这个场景来说一下。大家看看下面的图:
image.png

其实这就是分段加锁。你想,假如你现在iphone有1000个库存,那么你完全可以给拆成20个库存段,要是你愿意,可以在数据库的表里建20个库存字段,比如stock_01,stock_02,类似这样的,也可以在redis之类的地方放20个库存key。

总之,就是把你的1000件库存给他拆开,每个库存段是50件库存,比如stock_01对应50件库存,stock_02对应50件库存。

接着,每秒1000个请求过来了,好!此时其可以是自己写一个简单的随机算法,每个请求都是随机在20个分段库存里,选择一个进行加锁。

bingo!这样就好了,同时可以有最多20个下单请求一起执行,每个下单请求锁了一个库存分段,然后在业务逻辑里面,就对数据库或者是Redis中的那个分段库存进行操作即可,包括查库存 -> 判断库存是否充足 -> 扣减库存。

这相当于什么呢?相当于一个20毫秒,可以并发处理掉20个下单请求,那么1秒,也就可以依次处理掉20 * 50 = 1000个对iphone的下单请求了。

一旦对某个数据做了分段处理之后,有一个坑大家一定要注意:就是如果某个下单请求,咔嚓加锁,然后发现这个分段库存里的库存不足了,此时咋办?

这时你得自动释放锁,然后立马换下一个分段库存,再次尝试加锁后尝试处理。这个过程一定要实现。

分布式锁并发优化方案有没有什么不足?

不足肯定是有的,最大的不足,大家发现没有,很不方便啊!实现太复杂了。

  • 首先,你得对一个数据分段存储,一个库存字段本来好好的,现在要分为20个分段库存字段;
  • 其次,你在每次处理库存的时候,还得自己写随机算法,随机挑选一个分段来处理;
  • 最后,如果某个分段中的数据不足了,你还得自动切换到下一个分段数据去处理。

这个过程都是要手动写代码实现的,还是有点工作量,挺麻烦的。

不过我们确实在一些业务场景里,因为用到了分布式锁,然后又必须要进行锁并发的优化,又进一步用到了分段加锁的技术方案,效果当然是很好的了,一下子并发性能可以增长几十倍。

主从复制

为了负担读压力,redis支持主从复制。redis的主从复制可以根据是否是全量分为全量同步和增量同步

全量同步

redis全量复制一般发生在slave初始化阶段,这是slave需要将master上的所有数据都复制一份,具体步骤如下:

  1. 从服务器连接到主服务器,发送SYNC命令
  2. 主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有命令;
  3. 主服务器BGSAVE执行完后,向所有的从服务器发送快照文件,并在发送期间继续记录被执行的写命令
  4. 从服务器收到快照文件胡丢弃所有旧数据,载入收到的快照
  5. 主服务器发送完毕后开始向从服务器发送缓冲区的写命令
  6. 从服务器完成对快照的载入,哈开始接收命令请求,并执行来自主服务器缓冲区的写命令

image.png

万传给你上述几个步骤后就完成了从服务器数据初始化的所有操作,从服务器此时可以接收来自用户的读请求

增量同步

redis增量复制是指slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。增量复制的过程主要是主服务器没执行一个写命令就会想服务器发送相同的写命令。从服务器接收并执行收到的写命令

主从同步策略

主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。

当然,如果有需要,slave在任何时候都可以发起全量同步。而redis的策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步

特点

采用异步复制

一个主redis可以含有多个从redis

每个从redis可以接受来自其他从redis服务器的连接

主从复制对于主redis服务器来说非阻塞的,这意味着当从服务器在进行主从复制同步过程中,主redis仍然可以处理外界的访问请求

主从复制对于从redis服务器来说也是非阻塞的,这意味着即使从redis在进行主从复制过程中也可以接收外界的读请求,只不过这时从redis 返回的是以前老的数据。如果不想这样,那么可以在启动redis之前,修改配置文件,从redis复制同步过程中来自外界的查询请求都会返货错误信息给客户端

主从复制提高了redis服务的扩展性,避免单个redis服务器的读写访问压力过大的问题,同时也可以给为数据备份及冗余提供一种解决方案

为了编码主redis服务器写磁盘压力带来的开销,可以配置让主redis不再将数据持久化到磁盘,而是通过连接让一个配置的从redis服务器及时的将相关数据持久化到磁盘。不过这样会存在一个问题,就是主redis 服务器一旦重启,因为主redis服务器数据为空,这时候通过主从同步可能导致从redis服务器上的数据也被清空

主从复制需要注意的几个问题

在全量同步过程中,master会将数据保存在rdb文件中然后发送给slave服务器,但是如果master上的磁盘空间无效怎么办?那么此时全量同步对于master来说将是一根十分有压力的操作了。此时可以通过无盘复制来达到目的,由master直接开启一个socket将rdb文件发送给slave服务器(无盘复制一般应用在磁盘空间有限,但是网络状态良好的情况下)

主从复制结构,一般slave服务器不能进行写操作,但是这不是死的,之所以这样是为了更容易的保证主服务器和各个从服务器之间数据的一致性,如果slave服务器上数据进行了修改,那么要保证所有主从服务器都能一致,虽然能够通过配置让从服务器有写操作,但是不建议

主从服务器之间会定期进行通话,如果主服务器配置了密码需要在从服务器上进行配置,否则会导致从服务器不能访问主服务器

关于slave服务器上过期键的处理,一般由master服务器复制键的过期删除处理,然后将相关删除命令以同步的方式发给从服务执行

当不进行主节点持久化时,应将主节点的自动重启功能关闭,避免在从节点复制时,主节点重启导致复制了空集

哨兵模式

哨兵是用于监控redis集群中master状态的工具,是redis的高可用性解决方案,已集成到redis2.4之后的版本中。

sentinel,即哨兵,可以监视一个或者多个redis master服务,以及这些master服务的所有从服务。当某个master服务下线后,自动将该master下的某个从服务升级为master服务替代已下线的master服务继续处理请求。

可配置多个哨兵,除了监控主从实例外,还会互相进行监控,形成多哨兵模式

配置多哨兵模式后,假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行故障切换(failover),仅仅是哨兵1主观的认为主服务器不可用,这个现象称为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover操作。切换成功后,就会通过发布-订阅模式,让各个哨兵把自己监控的从服务器实现且换主机,这个过程称为客观下线

原理

哨兵通过发送命令,等待redis服务器响应,从而监控运行的多个redis实例

一个主节点,两个从节点,三个哨兵

监控工作流程

image.png

哨兵们给主从服务器发送info指令,并与主服务器建立cmd连接,每个哨兵之间互相ping

配置

首先配置redis的主从服务器,修改redis.conf文件:

  1. # 主服务器的配置
  2. # 使得Redis服务器可以跨网络访问
  3. bind 0.0.0.0
  4. # 设置密码
  5. requirepass "123456"
  6. # 从服务器的配置
  7. # 指定主服务器,注意:有关slaveof的配置只是配置从服务器,主服务器不需要配置
  8. slaveof 192.168.11.128 6379
  9. # 主服务器密码,注意:有关slaveof的配置只是配置从服务器,主服务器不需要配置
  10. masterauth 123456

配置哨兵,复制sentinel.conf进行备份,然后修改

  1. # 对外服务端口号
  2. port 26379
  3. # 存储哨兵的工作信息
  4. dir /tmp
  5. # 禁止保护模式
  6. protected-mode no
  7. # 配置监听的主服务器,这里sentinel monitor代表监控,mymaster代表服务器的名称,可以自定义,192.168.11.128代表监控的主服务器,6379代表端口,2代表只有两个或两个以上的哨兵认为主服务器不可用的时候,才会进行failover操作。
  8. sentinel monitor mymaster 192.168.11.128 6379 2
  9. #哨兵连接主节点多长时间没有响应就代表挂了。后边30000是毫秒,也就是30秒。
  10. sentinel down-after-milliseconds mymaster 30000
  11. # 这个配置项是指在故障转移时,最多有多少个从节点对新的主节点进行同步。这个值越小完成故障转移的时间就越长
  12. # 这个值越大就意味着越 多的从节点因为同步数据而不可用。
  13. sentinel parallel-syncs mymaster 1
  14. #在进行同步的过程中,多长时间完成算有效,系统默认值是3分钟。
  15. sentinel failover-timeout mymaster 180000
  16. # sentinel author-pass定义服务的密码,mymaster是服务名称,123456是Redis服务器密码
  17. # sentinel auth-pass <master-name> <password>
  18. sentinel auth-pass mymaster 123456

最后进入redis安装目录下的src目录,使用命令启动主从服务器和哨兵,需要注意一下顺序,先主,后从,再哨兵

  1. # 启动Redis服务器进程
  2. ./redis-server ../redis.conf
  3. # 启动哨兵进程
  4. ./redis-sentinel ../sentinel.conf

redis常见问题

单线程的 Redis 为什么这么快?

◆纯内存操作

◆单线程操作,避免了频繁的上下文切换

◆采用了非阻塞 I/O 多路复用机制

Redis 的过期策略以及内存淘汰机制

这个问题相当重要,到底 Redis 有没用到家,这个问题就可以看出来。

比如你 Redis 只能存 5G 数据,可是你写了 10G,那会删 5G 的数据。怎么删的,这个问题思考过么?

还有,你的数据已经设置了过期时间,但是时间到了,内存占用率还是比较高,有思考过原因么?

回答:Redis 采用的是定期删除+惰性删除策略。

为什么不用定时删除策略

定时删除,用一个定时器来负责监视 Key,过期则自动删除。虽然内存及时释放,但是十分消耗 CPU 资源。

在大并发请求下,CPU 要将时间应用在处理请求,而不是删除 Key,因此没有采用这一策略。

定期删除+惰性删除是如何工作

定期删除,Redis 默认每个隔100ms 检查,是否有过期的 Key,有过期 Key 则删除。

需要说明的是,Redis 不是每隔 100ms 将所有的 Key 检查一次,而是随机抽取进行检查(如果每隔 100ms,全部 Key 进行检查,Redis 岂不是卡死)。

因此,如果只采用定期删除策略,会导致很多 Key 到时间没有删除。于是,惰性删除派上用场。

也就是说在你获取某个 Key 的时候,Redis 会检查一下,这个 Key 如果设置了过期时间,那么是否过期了?如果过期了此时就会删除。

采用定期删除+惰性删除就没其他问题了么?

不是的,如果定期删除没删除 Key。然后你也没即时去请求 Key,也就是说惰性删除也没生效。这样,Redis的内存会越来越高。那么此时就应该采用内存淘汰机制。

在 redis.conf 中有一行配置:

maxmemory-policy volatile-lru

该配置就是配内存淘汰策略的(什么,你没配过?好好反省一下自己):

◆noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。应该没人用吧。

◆allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 Key。推荐使用,目前项目在用这种。

◆allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 Key。应该也没人用吧,你不删最少使用 Key,去随机删。

◆volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 Key。这种情况一般是把 Redis 既当缓存,又做持久化存储的时候才用。不推荐。

◆volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 Key。依然不推荐。

◆volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 Key 优先移除。不推荐。

PS:如果没有设置 expire 的 Key,不满足先决条件(prerequisites);那么 volatile-lru,volatile-random 和 volatile-ttl 策略的行为,和 noeviction(不删除) 基本上一致。

Redis 和数据库双写一致性问题

一致性问题是分布式常见问题,还可以再分为最终一致性和强一致性。数据库和缓存双写,就必然会存在不一致的问题。

答这个问题,先明白一个前提。就是如果对数据有强一致性要求,不能放缓存。我们所做的一切,只能保证最终一致性。

另外,我们所做的方案从根本上来说,只能说降低不一致发生的概率,无法完全避免。因此,有强一致性要求的数据,不能放缓存。

回答:首先,采取正确更新策略,先更新数据库,再删缓存。其次,因为可能存在删除缓存失败的问题,提供一个补偿措施即可,例如利用消息队列。

如何应对缓存穿透和缓存雪崩问题

这两个问题,说句实在话,一般中小型传统软件企业,很难碰到这个问题。如果有大并发的项目,流量有几百万左右。这两个问题一定要深刻考虑。

缓存穿透,即黑客故意去请求缓存中不存在的数据,导致所有的请求都怼到数据库上,从而数据库连接异常。

缓存穿透解决方案:

◆利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试。

◆采用异步更新策略,无论 Key 是否取到值,都直接返回。Value 值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。需要做缓存预热(项目启动前,先加载缓存)操作。

◆提供一个能速判断请求是否有效的拦截机制,比如,利用布隆过滤器,内部维护一系列合法有效的 Key。迅速判断出,请求所携带的 Key 是否合法有效。如果不合法,则直接返回。

缓存雪崩,即缓存同一时间大面积的失效,这个时候又来了一波请求,结果请求都怼到数据库上,从而导致数据库连接异常。

缓存雪崩解决方案:

◆给缓存的失效时间,加上一个随机值,避免集体失效。

◆使用互斥锁,但是该方案吞吐量明显下降了。

◆双缓存。我们有两个缓存,缓存 A 和缓存 B。缓存 A 的失效时间为 20 分钟,缓存 B 不设失效时间。自己做缓存预热操作。

然后细分以下几个小点:从缓存 A 读数据库,有则直接返回;A 没有数据,直接从 B 读数据,直接返回,并且异步启动一个更新线程,更新线程同时更新缓存 A 和缓存 B。

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

这个问题大致就是,同时有多个子系统去 Set 一个 Key。这个时候大家思考过要注意什么呢?

需要说明一下,我提前百度了一下,发现答案基本都是推荐用 Redis 事务机制。

我并不推荐使用 Redis 的事务机制。因为我们的生产环境,基本都是 Redis 集群环境,做了数据分片操作。

你一个事务中有涉及到多个 Key 操作的时候,这多个 Key 不一定都存储在同一个 redis-server 上。因此,Redis 的事务机制,十分鸡肋。

如果对这个Key 操作,不要求顺序

这种情况下,准备一个分布式锁,大家去抢锁,抢到锁就做 set 操作即可,比较简单。

如果对这个 Key 操作,要求顺序

假设有一个 key1,系统 A 需要将 key1 设置为 valueA,系统 B 需要将 key1 设置为 valueB,系统 C 需要将 key1 设置为 valueC。

期望按照 key1 的 value 值按照 valueA > valueB > valueC 的顺序变化。这种时候我们在数据写入数据库的时候,需要保存一个时间戳。

假设时间戳如下:

系统A key 1 {valueA 3:00}
系统B key 1 {valueB 3:05}
系统C key 1 {valueC 3:10}

那么,假设这会系统 B 先抢到锁,将 key1 设置为{valueB 3:05}。接下来系统 A 抢到锁,发现自己的 valueA 的时间戳早于缓存中的时间戳,那就不做 set 操作了,以此类推。

其他方法,比如利用队列,将 set 方法变成串行访问也可以。总之,灵活变通。