1.Redis基础

1.何为Redis?

Redis是一个基于C语言开发的 内存数据库,Redis的缓存能力大小受限于内存的大小。由于Redis基于内存的这一特性,使得它具有极高的读写速度。

2.Redis应用场景?

数据缓存:缓存热点数据、万物皆可缓存。
分布式锁:Redis集群情况下,保证高并发访问数据一致性问题。
数据统计:统计在线用户、点赞、评论等信息。
消息队列:Redis中的发布/订阅模式。
服务限流:Redis中可以通过Lua脚本实现服务限流、保障系统的正常运行。

3.Redis&Memcached?

相同点:两者都具有高性能的内存缓存能力、且都有各自的缓存策略。
不同点:
1、数据类型:Redis支持多种数据类型,Memcached支持少量的数据类型。
2、容灾与持久化:Redis中具备一定的容灾能力,通过数据持久化到磁盘进行数据恢复。
3、过期策略:Redis中采用了定期删除和惰性删除两种机制、但memcached中只提供了惰性删除。
4、集群方式:Redis中可以更加方便的进行集群,但memached只能借助第三方进行集群设置。
5、事务、脚本、模型:Redis中支持事务、支持Lua脚本、支持发布/订阅模式。
6、线程模型:Redis中为单线程多路IO复用(Redis6中引入了多线程),memcached为多线程非阻塞IO模型。

4.Redis数据类型

String:Redis中的String类型不仅可以存储字符串信息、也可以存储二进制类型。通常用于访问统计、点赞等。
List、Hash、Set、ZSet、地理位置(Geography、HyperLogLog)。

2.Redis线程模型

Redis6.0之前采取的是 单线程非阻塞IO多路复用模型。而在Redis6.0时,也开始支持多线程。

1.为何采用单线程

简单易维护
单线程中无需考虑多线程情况下的资源并发访问问题,而且单线程更加容易实现。
避免频繁的上下文切换
多线程机制中必然需要在CPU之间进行频繁的线程上下文切换,每一次切换都要涉及PC寄存器、堆栈指
针等的变动。
如果使用单线程机制,则完全不需要考虑多线程环境下造成资源的频繁调度。
Redis性能在于内存和网络

2.为何6中引入了多线程

网络是影响Redis中性能的一个关键性因素之一、引入多线程就是为了提高多网络IO的处理速度,以此来提高Redis的网络读写能力。

3.Redis过期策略

Redis中提供了两种过期策略:一种是定期删除,一种是惰性删除的机制。
Redis中通过维护一个过期字典来管理所有的key的过期时间。
定期扫描:
由于Redis是单线程的,为了防止占用过多的处理时间、在定期扫描机制中采取了一种指定随机数量的删除策略(随机取出指定集的过期key进行删除)。
惰性删除:
Redis中如果只进行频繁的定期扫描机制,仍然无法满足Redis中过多垃圾处理问题,通过惰性删除机制,可以在访问该key时,才进行过期检测,进而进行删除。

4.Redis淘汰机制

Redis中提供了8中淘汰机制、最常使用的是allkeys-lru。
allkeys-lru:Redis内存不足以容纳新数据时,则会在过期key中随机删除最近最少使用的key。
image.png
LRU算法:
LRU算法中不仅需要key/value字典、同时还会附加一个链表、链表中的元素按照一定顺序进行存储、当新元素加入时,会将尾部的元素剔除掉、元素被访问时,则会移动到链表头部。

5.Redis三大问题

1.缓存穿透

1.问题

Redis缓存穿透指的是大量并发请求未命中Redis,Redis中不存在,且DB中也不存在。此时,大量请求打到数据库中,造成服务器压力剧增。
image.png

2.解决方案

缓存空值:当Redis中没有命中时,则可以通过缓存null值来进行解决
布隆过滤器:布隆过滤器指的是通过维护一个二进制列表来过滤那些key一定不存在、但无法确定已存在的key是否过期。通过hash函数来设置对应位置的0改为1。

2.缓存击穿

1.问题

Redis缓存击穿指的是:Redis中不存在key(过期),但DB中存在。此时,大量请求到达缓存,缓存击穿,之后大量请求压到数据库中,造成服务压力剧增。
image.png

2.解决方案

永久有效:Redis缓存数据时,可以设置为永不过期来解决Redis缓存击穿问题
互斥锁:Redis未命中后、将第一个Redis请求添加互斥锁、其他请求等待、当第一个请求获取数据并同步到Redis中后其他线程直接走缓存。
熔断、降级:

3.缓存雪崩

1.问题

Redis缓存雪崩指的是大量并发请求访问Redis时、Redis中的key大量过期、导致请求打到DB中。
image.png

2.解决方案

分散过期:热点数据分散处理过期、防止大量请求请求不到数据。
加锁/队列:

6.Redis持久化机制

Redis中提供了两种持久化机制,一种是RDB机制、一种是AOF机制。这两种机制都支持将Redis内存中的数据持久化到磁盘文件中,便于启动后恢复数据。

1.RDB机制

RDB指的是在某个特定的时间点上通过快照的形式复制数据。是Redis中默认开启的持久化机制。耗时、耗性能、不可控、精度低。
image.png
redis.conf配置:(如需关闭,可直接注释掉下面三种配置)

  1. # 900 秒(15分钟)之后,如果至少有 1 key 发生变化,Redis 就会自动触发 BGSAVE 命令创建快照
  2. save 900 1
  3. # 300 秒(5分钟)之后,如果至少有 10 key 发生变化,Redis 就会自动触发 BGSAVE 命令创建快照
  4. save 300 10
  5. # 60 秒(1分钟)之后,如果至少有 10000 key 发生变化,Redis 就会自动触发 BGSAVE命令创建快照
  6. save 60 10000

RDB持久化过程:
1、Redis调用fork()函数开启一个子线程、主线程不进行任何操作。
2、子线程将数据集写入一个REB二进制文件中。
3、子线程写完数据后,将旧的RDB文件替换掉。

2.AOF机制

AOF机制指的是将执行的命令通过追加写入的方式写入到.aof文件中的机制。时效高、耗内存、效率低、但精度高。
image.png
redis.conf中的配置:

  1. appendonly no

AOF持久化过程:
Redis中没执行一条Redis命令就追加写入AOF文件中。
Redis中文件过大则会进行AOF重写。AOF机制中,提供了三种同步机制供用户使用。

3.两者对比

RDB:二进制文件、 快速、耗内存、但精度低。
AOF:日志追加文件、速度慢、耗内存、但精度高。

4.AOF重写

Redis中的AOF日志追加写入过程中,可能出现文件过大情况,此时,则需要通过压缩重写来缩减AOF文件。
image.png
1、AOF文件达到一定条件、主进程调用系统函数fork()创建一个大小一致的子进程。
2、子进程调用aof_rewrite()函数进行重写操作,主进程继续提供服务。
3、当子进程重写完成后,会响应一个信号给主进程。
4、子进程重写期间、父进程收到新命令则会写入aof缓冲区、子进程写完后再次同步。
5、主进程调用信号处理函数对aof文件进行替换覆盖。

7.Redis数据一致性

Redis和MySQL为了保持双写一致性、存在四种同步机制。基本流程如下:
查询:线程查询Cache、缓存未命中;

1.四种机制

1、先更新Cache、再更新DB
2、先更新DB、在更新Cache
3、先删除Cache、再更新DB
4、先更新DB、再删除Cache

2.方案可行性

前两种方案中都可能存在更新失败问题。不能很好的保持数据一致性。
先删Cache、再更新DB:
1、线程A删除了Cache、准备去更新DB,线程阻塞;
2、此时、线程B读取缓存,未命中,查询DB;
3、线程B更新旧的DB数据到Cache中;
4、线程A恢复、发现Cache中数据不一致。
先更新DB、再删除Cache:
删除失败:
image.png
1、线程A更新DB、删除Cache失败
2、线程B查询到Cache中旧的数据
删除未失败:
image.png
1、线程A更新DB、此时Cache失效
2、线程B当前数据、未命中Cache
3、线程B读取DB旧值、更新Cache(A写入加锁、B读取快速)
4、线程A删除Cache、更新Cache
总结:
总体来说,先更新DB、再删除Cache较好,不容易出现数据不一致性。

3.同步方案

搭建Slave从服务解析Binlog日志、将更新消息推入到Kafka消息队列中,下游服务进行拉取消费。
具备与基本业务完全解耦、高可靠性、无时效性问题等。

8.Redis分布式锁

分布式锁:分布式系统中、为了防止不同进程间访问临界资源产生并发问题提出的一种解决思想。

1.使用场景

分布式系统的 秒杀下单、抢红包等业务中具有特别重要的作用

2.锁的特征

image.png 互斥:同一时间、单个锁只能被单个进程占用
锁超时:长时间占用锁应该释放、防止造成死锁
可重入:同一个进程可多次获取同一把锁
高可用:引入锁后、应该竟可能的降低锁对性能的消耗
安全性:占用锁对象后、只能由占用锁的客户端进行释放。

3.实现方案

1.方案1-SETNX+EXPIRE

这种方案中、通过setnx命令获取锁、通过expire设置过期时间。代码如下:

  1. ifjedis.setnx(key_resource_id,lock_value) == 1){ //加锁
  2. expirekey_resource_id100); //设置过期时间
  3. try {
  4. do something //业务请求
  5. }catch(){
  6. }
  7. finally {
  8. jedis.del(key_resource_id); //释放锁
  9. }
  10. }

执行过程:
1、setnx(key、value)获取锁、setnx成功会返回1
2、expire(key、time)来设置过期时间、超过该时间则自动释放
3、del(key)正常释放锁
存在问题:
执行业务时间过长、在expire自动释放锁之前、服务切断、导致无法锁超时释放。此时,进程会进入到死锁中。
原因是因为:setnx+expire是两条指令、并非原子操作。

2.方案2-SETNX-VALUE(扩展参数)

这种方案通过将过期时间加入到setnx的参数中、无序单独设置过期时间。
释放锁时、通过比较该参数值是否一致、然后进行删除。
存在问题:
参数值比较与删除并不是原子性操作、Redis中也没有提供相关delifequals指令。
这时候就需要Lua脚本执行

3.方案3-Lua脚本

Lua脚本保证了setnx中的参数一致性与删除命令的原子性操作。

  1. if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then
  2. redis.call('expire',KEYS[1],ARGV[2])
  3. else
  4. return 0
  5. end;

4.方案4-第三方框架

Redisson、ReadLock都可以提供分布式锁的实现