redis是一个基于内存的高性能的 key-value 数据库。
直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
Redis底层的数据结构包括:简单动态数组SDS、链表、字典、跳表、整数集合、压缩列表、对象。压缩列表是一种为了节约内存而开发的且经过特殊编码之后的连续内存块顺序型数据结构。
redisObject是Redis类型系统的核心,数据库中的每个键、值,以及Redis本身处理的参数,都表示为这种数据类型。
//Redis 对象
typedef struct redisObject {
// 类型,记录了对象所保存的值的类型
unsigned type:4
// 对齐位
unsigned notused:2;
// 编码方式,记录了对象所保存的值的编码
unsigned encoding:4;
// LRU 时间(相对于 server.lruclock)
unsigned lru:22;
// 引用计数
int refcount;
// 指向对象的值,指向实际保存值的数据结构, 这个数据结构由type属性和encoding属性决定
void *ptr;
} robj;
下图展示了 redisObject 、Redis 所有数据类型、以及 Redis 所有编码方式(底层实现)三者之间的关系:
struct sdshdr {
int len; // 记录buf数组中已使用字节的数量,等于SDS所保存字符串的长度
int free; // 记录buf数组中未使用字节的数量
char buf[];// 字节数组,用于保存字符串
};
C字符串 | SDS |
---|---|
获取字符串长度的复杂度为O(N) | 获取字符串长度的复杂度为O(1) |
API是不安全的,可能会造成缓冲区溢出 | API是安全的,不会造成缓冲区溢出 |
修改字符串长度N次必然需要执行N次内存重分配 | 修改字符串长度N次最多需要执行N次内存重分配 |
只能保存文本数据 | 可以保存文本数据或者二进制数据 |
可以使用所有的 |
可以使用一部分 |
String:key -value缓存应用,最常规的set/get操作,value可以是String也可以是数字。一般做一些复杂的计数功能的缓存。短信验证码
Hash:field-value映射表,存储用户信息和商品信息,单点登录;
List:list分页查询,消息队列,粉丝列表、文章的评论列表功能;
Set:实现差,并,交集操作,全局去重的功能;
Sorted set:用户列表,礼物排行榜,弹幕消息的功能;
HyperLogLog:通常用于基数统计。使用少量固定大小的内存,来统计集合中唯一元素的数据。统计结果不是精确值,而是一个带有0.81%标准差(standard error)的近似值。所以,HyperLogLog适用于一些对于统计结果精确度要求不是特别高的场景,例如网站的UV统计。
Geo:Redis 3.2版本的新特性。可以将用户给定的地理位置信息储存起来,并对这些信息进行操作。
获取2个位置的距离,根据给定地地理位置坐标获取指定范围内的地址位置集合。
BitMap:位图。
Stream:主要用于消息队列,类似于 Kafka,可以认为是 pub/sub 的进阶版。提供了消息的持久化和主从复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。
缓存、分布式锁、排行榜(zset)、计数(incrby)、消息队列(stream)、地理位置(geo)、访客统计(hyperloglog)等。
一般使用list结构作为队列,rpush生产消息,lpop消费消息。
缺点:在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如rabbitmq等
问题:能不能生产一次消费多次呢?
使用pub/sub主题订阅者模式,可以实现1:N的消息队列。
更新策略:采用正确更新策略,先更新数据库,再删除缓存。其次,因为可能存在删除缓存失败的问题,提供一个补偿措施即可,例如利用消息队列。
采用延时双删策略:
注:如果对数据有强一致性要求,不能放缓存。
由于Redis是一种内存型数据库,即服务器在运行时,系统为其分配了一部分内存存储数据,一旦服务器挂了或宕机了,那么数据库里面的数据将会丢失,为了使服务器即使突然关机也能保存数据,必须通过持久化的方式将数据从内存保存到磁盘中。
持久化就是把内存的数据写到磁盘中,防止服务器宕机了,导致内存数据待久
。
Redis提供两种持久化机制,分别是RDB和AOF。Redis服务器默认开启RDB,关闭AOF;
RDB(Redis DataBase):快照。按照一定的时间将内存的数据以快照的形式保存到硬盘中,对应产生的数据文件为dump.rdb。通过配置文件中的save参数来定义快照的周期。
优点:RDB文件紧凑,体积小,网络传输快,适合全量复制;恢复速度比AOF快。与AOF相比,RDB最重要的优点之一是对性能的影响相对较小。
缺点:RDB文件的致命缺点在于其数据快照的持久化方式决定了必然做不到实时持久化,而在数据越来越重要的今天,数据的大量丢失很多时候是无法接受的,因此AOF持久化成为主流。此外,RDB文件需要满足特定格式,兼容性差(如老版本的Redis不兼容新版本的RDB文件)。
AOF(Append Only File):将Redis执行的每次写命令记录到单独的日志文件中,当重启Redis会重新将持久化的日志志中文件恢复数据。
与RDB持久化相对应,AOF的优点在于支持秒级持久化、兼容性好,缺点是文件大、恢复速度慢、对性能影响大。
当Redis的内存(maxmemory参数配置)已满时,它会根据淘汰策略(maxmemory-policy参数配置)进行相应的操作。
no-eviction:不删除策略。Redis默认策略。达到最大内存限制时,若需要更多内存,直接返回错误信息。
allkeys-lru:所有key通用;优先删除最近最少使用的key
volatile-lru:只限于设置了 expire 过期时间的部分;优先删除最近最少使用的key
allkeys-random:所有key通用;随机删除一部分key。
volatile-random:只限于设置 expire 的部分;随机删除一部分key。
volatile-ttl:只限于设置 expire 的部分;优先删除剩余时间短的key。
volatile-lfu:只限于设置 expire 的部分;优先删除最不经常使用的key。
allkeys-lfu:所有key通用;优先删除最不经常使用的key。
volatile-*:从已过期时间的数据集中淘汰key。
allkeys-*:所有key。
Redis是 key-value 数据库,可以设置Redis缓存的key的过期时间。Redis的过期删除策略就是指当Redis中的key过期了,Redis是如何进行处理的。
定时删除:在设置key的过期时间的同时,Redis会创建一个定时器,当key达到过期时间时,立即删除该键。
惰性删除:放任键过期不管,只有当获取键时,才检查获取的键是否过期,若过期删除该键;若没过期,就返回值。
定期删除:每隔一段时间(默认100ms),程序就对数据库进行一次检查,删除过期键。
注:expires字典会保存所有设置了过期时间的key的过期时间数据。key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。
Redis采用的是定期删除和惰性删除策略
。
Redis基于 Reactor 模式开发了自己网络事件处理器,它由四部分组成,分别是套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。
套接字
IO多路复用
文件事件分派器
事件处理器
IO多路复用:"多路"是指多个TCP连接;"复用"是指复用一个或多个线程;可以让单线程高效的处理多个连接请求。
IO多路复用使用两个系统调用(select/poll/epoll和recvfrom),阻塞IO只调用了recvfrom;select/poll/epoll 核心是可以同时处理多个连接,而不是更快,所以连接数不高的话,性能不一定比多线程+阻塞IO好,多路复用模型中,每一个socket,设置为non-blocking,阻塞是被select这个函数阻塞的,而不是被socket阻塞的。
原理:客户端操作服务器时会三种文件描述符(简称fd)
:writefds(写)、readfds(读)、exceptfds(异常)
。select会阻塞监视3类文件描述符,等有数据、可读、可写、出异常或超时,就会返回;返回后通过遍历fdset整个数组来找到就绪的描述符fd,然后进行对应的IO操作。
优点:所有平台都支持,跨平台性好。
缺点:
原理:基本原理与select一致,也是轮询+遍历;唯一的区别就是poll没有最大文件描述符限制(使用链表的方式存储fd)。
原理:epoll也没有fd个数限制,用户态拷贝到内核态只需要一次,使用时间通知机制来触发。通过epoll_ctl注册fd,一旦fd就绪就会通过callback回调机制来激活对应fd,进行相关的io操作。
优点:
例子:100万个连接,里面有1万个连接是活跃,我们可以对比 select、poll、epoll 的性能表现
select:不修改宏定义默认是1024,l则需要100w/1024=977个进程才可以支持 100万连接,会使得CPU性能特别的差。
poll: 没有最大文件描述符限制,100万个链接则需要100w个fd,遍历都响应不过来了,还有空间的拷贝消耗大量的资源。
epoll: 请求进来时就创建fd并绑定一个callback,主需要遍历1w个活跃连接的callback即可,即高效又不用内存拷贝。
定义:同一时间内大量键过期(失效),导致所有请求瞬间都落在了数据库中导致连接异常而崩掉。
如何解决缓存雪崩
给缓存数据的过期时间设置随机机,防止同一时间大量数据过期。
给每一个缓存数据增加相应的缓存标记,记录缓存是否失效,若标记失效,则更新缓存数据。
并发量不大时,可以使用加锁排队。
对于"Redis挂掉了,请求全部走数据库"这种情况,我们可以有以下的思路:
事发前:实现Redis的高可用(主从架构+Sentinel或者Redis集群),尽量避免Redis挂掉
事发中:设置本地缓存(ehcache)+限流(hystrix),尽量避免数据挂掉,保证服务能正常工作
事发后:redis持久化,重启后自动从磁盘上加载数据,快速恢复缓存数据
定义:恶意请求缓存中不存在的数据,导致所有请求都落在数据库,造成短时间承受大量请求而崩掉
如何解决缓存穿透
采用布隆过滤器
,将所有可能存在的数据哈希到一个bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免对底层存储系统的查询压力。定义:缓存击穿是指缓存中没有但数据库中有的数据。同时读缓存数据没有读到,导致所有请求都落在数据库,造成过大压力。
缓存雪崩与缓存击穿的区别
与缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期,很多数据都查不到从而查数据库。
如何解决缓存击穿
设置热点数据永不过期
。利用互斥锁
:在缓存失效的时,先获取锁,得到锁后再去请求数据库。没有得到锁,则休眠一段时间在重试。定义:缓存预热指系统上线后,将相关的缓存数据直接加载到Redis中,这样就可以避免在用户请求时,先查询数据库,然后再将数据缓存的问题。
如何解决缓存预热
当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
缓存降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:
服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。
事务间相互独立:事务中的所有命令都会序列化,按顺序执行。事务在执行过程中,不会被其他客户端请求的命令中断。
注:
事务中的命令要么都执行,要么都不执行
。
Redis事务功能是通过MULTI、EXEC、DISCARD、WATCH命令实现的。通过MULTI开启事务,然后将请求的命令入队,最后通过EXEC命令依次执行队列中所有的命令。
Redis会将一个事务所有的命令序列化,然后按顺序执行。
Redis不支持回滚
。Redis在事务失败时不进行回滚,而是继续执行剩下的命令。若在一个事务中的命令出现错误,那么所有命令都不会执行
。若在一个事务中出现运行错误,那么正确的命令会被执行
。WATCH命令:是一个乐观锁,可以为Redis提供CAS操作。可以监控一个或多个键,一旦其中有一个键被修改或删除,之后的事务就不执行,监控一直持续到EXEC命令。
MULTI命令:用于开启事务
。MULTI执行后,Client可以继续向服务器发送任意多条命令,这些命令会存放到一个队列中,当EXEC命令调用后,所有队列中的命令才会被执行。
EXEC命令:执行所有事务块的命令,可以理解为提交事务。按命令的执行顺序,返回事务中所有命令的返回值。当操作被打断时,返回空值(nil)。
DISCARD命令:用于清空事务队列,并放弃执行事务
。Client从事务状态中退出。
UNWATCH命令:用于取消WATCH命令对所有key的监控
。
注:
事务执行过程中,若服务器收到有EXEC、DISCARD、WATCH、MULTI之外的请求,将会把请求放入队列中
。
Redis是单进程模式,队列技术将并发访问变为串行访问,且多个客户端对Redis的连接并不存在竞争关系,Redis可以使用setnx
命令实现分布式锁。
获取锁时调用setnx(setnx若设置值成功,返回1;设置失败,返回0)。锁的value值会随机生成一个UUID,在释放锁时,会通过UUID进行判断是否为对应的锁,若是该锁,则释放该锁;可以使用 expire
命令为锁添加一个超时时间,超过该时间则自动释放锁。
setnx key value
:只有在 key 不存在时,才将key设置为value值。
多个系统同时对一个key进行操作,最后执行的顺序和我们期望的顺序不同,导致结果不同。
问题:如何解决并发竞争问题?
答:可以通过Redis或Zookeeper实现分布式锁。
- Redis实现分布式锁:通过Redis中setnx命令可以实现分布式锁。当获取锁时,调用setnx加锁。锁的value值会随机生成一个UUID。在释放锁时,通过UUID进行判断是否为对应的锁,若是则释放锁。使用expire命令为锁添加一个超时时间,若超过该时间则自动释放锁。
- Zookeeper实现分布式锁:通过Zookeeper临时有序节点可以实现分布式锁。每个Client对某个方法加锁时,在Zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。通过判断有序节点中,序号是否为最小来获取锁;当释放锁时,只需要删除瞬时有序节点。
Redis 官方站提出了一种权威的基于 Redis 实现分布式锁的方式名叫 Redlock,此种方式比原先的单节点的方法更安全。
特性
安全性:互斥访问。只会有一个Client能拿到锁资源。
容错性:只要大部分Redi节点可以存活,就可以正常提供服务。
避免死锁:最终Client都可以拿到锁,不会出现死锁的情况。即使原本锁住某资源的Client crash了或者出现了网络分区
哨兵(Sentinel) 是 Redis 的高可用性解决方案:由一个或多个 Sentinel 实例组成的 Sentinel 系统可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器。
哨兵的作用:
哨兵的核心知识
当从数据库启动时,会向主数据库发送sync命令,主数据库接收到sync后开始在后台保存快照rdb,在保存快照期间收到的命令缓存起来,当快照完成时,主数据库会将快照和缓存的命令一块发送给从数据库。复制初始化结束。之后,主数据库每收到1个命令就同步发送给从数据库。 当出现断开重连后,2.8之后的版本会将断线期间的命令传给从数据库。增量复制。
主从复制是乐观复制,当客户端发送写执行给主数据库,主数据库执行完立即将结果返回客户端,并异步的把命令发送给从数据库,从而不影响性能。
参考文章: