关注wx:CodingTechWork
Redis介绍
概述
- Redis是NoSQL,是key-value分布式内存数据库。
缓存
- 缓存是将数据从慢的介质换到快的介质上,提高读写效率和性能,并降低数据库的读写成本。
- 内存的速度一般都远远大于硬盘的速度,大量请求数据库或远程应用时,会导致大量的时间消耗在调用上,从而降低系统应用调用效率,若使用缓存,则可以充分利用资源,提高系统调用效率。
特点
- Redis支持数据的持久化:Redis运行在内存中,但可以持久化到磁盘。将内存中的数据保存到磁盘中,重启时可以再次加载进行使用。
- Redis提供多种数据结构类型存储,如list、set、zset、hash。
- Redis支持数据的备份,即主从(master-slave)模式的数据备份。
- Redis支持发布-订阅,通知key过期等特性。
- Redis性能高,读的速度是11万次/s,写的速度是8万次/s。
- Redis支持原子性,即要么全部执行成功,要么全部不执行。单个操作是原子的,多个操作也支持原子性事务,通过MULTI和EXEC指令包装。
热点数据和冷点数据
热数据
- 需要被计算节点频繁访问的在线类数据,如某导航信息,缓存后,会被读取很多次;
- 新建的通告信息,缓存后也会被读取很多次。
冷数据
- 对于理想类不经常访问的数据,如企业备份数据、业务与操作日志数据、话单与统计数据。
- 大部分数据可能还未再次访问就已被挤出内存,不仅占用内存,且价值不大。
Memcached和Redis区别
存储方式
:Memcached存储在内存中,断电挂掉丢失,数据不能超过内存大小;redis有部分存在硬盘上,也可以持久化数据到数据库中。数据支持类型
:Memcached所有的值均为简单的字符串,只是keyt-value类型数据;redis可提供list、set、zset、hash等数据结构存储。底层实现方式
:redis直接构建自己的虚拟机机制value值大小
:Memcached1MB,redis最大达1GB速度
:redis速度快于Memcached,因为redis内存操作、单线程操作、采用非阻塞I/O多路复用机制灾难恢复
:Memcached挂掉后,数据不可恢复;redis数据丢失后可通过AOF恢复。备份
:redis支持数据备份,即master-slave模式的数据备份。
Redis缓存问题
缓存雪崩
介绍
- 原有缓存失效,新缓存未到期。缓存中采用相同过期时间,同一时刻出现大面积缓存过期,原本访问Redis缓存的请求都直接去查询数据库,对数据库cpu和内存造成巨大压力,严重时造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃。
- 与缓存击穿区别是,雪崩是很多key采用相同过期时间,同时多个key失效。击穿是某一个key缓存失效。
解决方案
- 考虑加锁、或者队列的方式保证不会有大量的线程对数据库一次性读写,避免失效时大量的并发请求落到底层存储系统上。
- 缓存失效时间随机化,给每个key的失效时间加个随机值,保证数据不会在同一时间大面积失效。
- 设置热点数据永不过期,有更新操作直接更新缓存。如系统首页,有新产品展示,直接刷缓存,无需设置过期时间。
缓存穿透
介绍
- 查询一个不存在的数据:缓存和数据库中都没有数据,用户不断发起请求,在缓存中找不到,每次去数据库中再查一遍(两次无用查询),然后返回空。
- 缓存穿透一般出现在攻击场景下,攻击者知道请求路径的规则后,传递一些不存在的id进行查询数据。
解决方案
- 采用布隆过滤器(bloom-filter):将所有可能存在的数据哈希到一个足够大的bitmap中,不存在的数据会被这个bitmap过滤拦截掉,避免对底层存储系统查询压力。布隆过滤器用于检索一个元素是否在一个集合中,使用redisson可实现。底层是主要是初始化一个比较大数据,里面存放二进制0或1,一开始都是0,当一个key进来后经历3次hash计算,模于数组长度找到数据的下标然后把数组中原来的0改为1,三个
- 若一个查询返回数据为空,仍然将空结果进行缓存,过期时间可以设置很短(如30s,不超过5min),通过直接设置默认值放入Redis缓存,第二次查询缓存就可以获取值,不会继续访问数据库。
- 代码层面的接口层增加校验,如用户鉴权校验、参数校验等,不合法参数直接返回异常。
缓存击穿
介绍
- 对于一个key非常热点,不停的去访问这个key,承受高并发,当key失效瞬间,持续的高并发瞬间击穿缓存,直接请求数据库。
- 对于设置了过期时间的key,缓存在某个时间点过期的时候,恰好这个时间点对这个key有大量的并发请求进来,发现缓存过期直接从后端DB加载数据并回设到缓存,压垮DB。
解决方案
- 设置热点数据永远不过期。
- 加互斥锁,缓存失效是,不立即去load db,先使用如redis的setnx去设置一个互斥锁,当操作成功返回时在进行load db的操作并回设缓存,否则重试get缓存方法。分布式锁可以保证数据强一致性,但是性能不高。
- 逻辑过期:在设置key的时候,设置一个过期时间字段一块存入缓存中,不给当前key设置过期时间,当查询的时候,从redis取出数据后判断时间是否过期,瑞过期则开通另外一个线程进行数据同步,当前线程正常返回数据,这个数据是旧的,服务降级而已,适用于to c,无需保证数据强一致性时可以用。
解决思路
- 事前:Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃。
- 事中:本地 ehcache 缓存 + Hystrix 限流+降级,避免MySQL 被打死。
- 事后:Redis 持久化 RDB+AOF,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
双写一致性
- 方法一:使用redisson实现的读写锁,在读的时候添加共享锁,保证读读不互斥,读写互斥。当更新数据时,添加排他锁,读写、读读都互斥,保证在写数据的同时不会有其他线程读写数据,避免脏数据。底层使用setnx,保证同时只有一个线程操作锁住的方法。
- 方法二:延时双删,若是写操作,先删除缓存数据,然后更新数据库,最后再延时删除缓存中的数据,但这个延时玄学,也可能出现脏数据,并不能保证强一致性。
- 方法三:采用阿里的canal组件实现数据同步。部署一个canal服务,伪装成mysql的一个从节点,当mysql数据更新后,canal读取binlog数据,然后通过canal的客户端获取到数据更新缓存。
Redis数据类型
概述
String类型
:value可以是String也可以是数字,做一些复杂的计数功能缓存。hash类型
:value存放结构化对象,如单点登录时存储用户信息,以cookieId作为key,设置30min为缓存时间。list类型
:可做简单的消息队列。set类型
:可做全局去重功能。sorted set类型
:可按照score进行排序,可做排行榜应用。
字符串(string)
介绍
- redis的字符串是动态字符串,可修改。
- 采用预分配冗余空间来减少内存的频繁分配,内部为当前字符串分配的实际空间大小,一般要高于实际字符串长度。
- 当字符串长度小于1MB时,扩容都是加倍。字符串最大长度为512MB。
常用命令
get、set、incr、decr、mget
应用场景
计数器
:控制接口调用次数,如通过incrby命令进行递增。共享session
:分布式服务会将用户信息访问负载均衡到不同的节点上。限速
:如短信获取验证码功能,为了短信服务不会被频繁访问,限制用户每分钟获取验证码的频率、当天最大获取短信的次数。
散列(hash)
介绍
- hash是一个键值对集合,适合存储对象。
- hash内部是无序字典,内部存储多个键值对,采用渐进式的rehash策略,在rehash时,保留新旧两个hash结构,查询会同时查询新旧hash结构,渐进将旧hash内容迁移到新hash中。最终,旧hash被自动删除,内存被回收。
常用命令
hget、hset、hmget、hmset、hgetall
应用场景
- 存储对象:value存储为HashMap
集合(set)
介绍
- set是string类型的无序集合,通过散列表实现。
- set内部相当于一个特殊字典,所有value都一个NULL值。
- 当集合中最后一个元素被移除后,数据结构自动删除,内存被回收。
常用命令
sadd、srem、spop、sdiff、smemners、sunion
应用场景
- 去重数据功能,如用户访问列表查询。
有序集合(zset)
介绍
- zset类似set,区别是每个元素都会关联一个double类型的分数,通过分数来为集合的成员进行从小到大的排序。
- zset中的成员是唯一的,但分数可以重复。
- zset内部是使用“跳跃列表”的数据结构实现的。
- zset集合中最后一个元素被移除后,数据结构自动删除,内存被回收。
常用命令
zadd、zrange、zrem、zcard
应用场景
- 去重并自动排序的数据。
列表(list)
介绍
- list是简单的字符串列表,按照插入顺序排序,可增加一个元素到列表的头部或尾部。
- list的插入和删除速度快,索引定位慢。
- list中的每个元素之间都是使用双向指针顺序连接,同时支持前后遍历。
- 当最后一个元素被弹出后,数据结构自动删除,内存被回收。
常用命令
lpush、rpush、lpop、rpop、lrange、blpop
应用场景
- 消息队列,如最新通知排名,利用list的push操作,将任务存list中,再通过工作线程用pop操作将任务取出执行。
Redis原理
Redis单线程
为何Redis是单线程
- Redis是基于内存操作,CPU不是瓶颈,最大的瓶颈可能是机器内存的大小或者网络带宽,采用队列技术将并发访问变成串行访问。
- 绝大部分请求是纯粹的内存操作;
- 采用单线程避免不必要的上下文切换和竞争条件。
- 非阻塞I/O速度快,支持丰富数据类型,如String、list、set、sorted set、hash。
支持事务,操作都是原子性,具有丰富的特性,如可用于缓存、消息队列、按key设置过期时间。
原子性
- 因为redis是单线程的,一个操作不可再分,要么都执行,要么都不执行。
- 多个命令在并发中不一定是原子的:如get set操作。使用redis事务或者redis+lua可以实现。
Redis过期策略
为何不采用定时删除?
- 用定时器负责监视key,过期则删除,虽然可以及时释放内存,但消耗cpu资源,在大并发情况下,cpu要将时间应用于处理各种请求,而不是删除key。
redis采用定期删除+惰性删除策略。
定期删除
:默认每隔100ms检查,是否有过期的key,有则删除,但不是所有key都检查一次,而是随机抽取进行检查,这种会造成很多key到时间未删除。惰性删除
:获取某个key会检查是否设置过期时间,过期,则删除;这样就可以比避免定期删除时遗留的未删除的key。
问题
- 问题:若定期删除没有删除到key,且没有及时请求该key,导致惰性删除未生效,导致redis内存越来越高。。
- 解决方案:采用内存淘汰机制,在redis.conf中配置maxmemory-policy volatile-lru,即从已设置过期时间的数据集中挑选出最近最少使用的数据进行淘汰。
分类
惰性删除
- 在设置该key过期时间后,不去管它,当需要该key时,会检查是否过期,若过期直接删除,否则,返回该key。
- 定期删除会导致很多过期key到了时间并没有被删除掉,所以需要惰性删除,主动检查过期的key,发现key过期立即删除,不会返回任何东西。
- 惰性删除属于零散处理。
定期删除
- 定期删除就是每隔一段时间,对一些key进行检查,删除里面过期的key。
- redis会将每个设置了国企时间的key都放入一个独立的字典中,后续会定期遍历这个字典来删除到期的key;默认是每秒进行十次过期扫描,所以是100ms/次扫描。
- 定期删除是采用简单的贪心策略(避免全部key扫描对CPU带来负载):从过期字典中随机扫描20个key,删除这20个key中已经过期的key,若过期的key比率超过1/4,就继续随机扫描。
- 定期删除属于集中处理。
- 模式分类:
1)SLOW模式:定时任务,执行频率默认为10hz,每次不超过25ms,通过修改配置文件redis.conf的hz选项来调整次数。
2)FAST模式:执行频率不固定,每次事件循环会尝试执行,但两次间隔不低于2ms,每次耗时不超过1ms。
Redis事务
介绍
- Redis事务功能通过MULTI、EXEC、DISCARD、WATCH四个原语实现。
- redis会将一个事务中所有命令序列化,然后按顺序执行。redis不能在一个事务执行过程中插入零一二客户端发出的请求。
- redis不支持回滚,redis在事务失败时,会继续执行剩余命令,内部可以保持简单且快速。
- 如果在一个事务中命令出现错误,则所有命令都不会执行。
- 如果一个事务中出现运行错误,则正确的命令会被继续执行。
四个原语
MULTI
- MULTI命令用于标记事务块的开始,将后续命令逐个放入队列中,然后才能使用EXEC命令原子化执行该命令序列。
- 返回值是一个简单的字符串,总是OK。
EXEC
- 在一个事务中执行所有先前放入队列的命令,然后恢复正常的连接状态。
- 使用WATCH命令时,只有当受监控的键未被修改时,才能使用EXEC命令执行事务块命令,采用CAS检查再设置的机制。
- 返回值是衣蛾数组,每个元素分别是原子化事务中每个命令的返回值。
- 当使用WATCH命令时,若事务执行中止,则返回一个Null值。
DISCARD
- 清除所有先前在一个事务块中放入队列的命令,然后恢复正常的连接状态。
- 返回值是一个简单的字符串,总是OK。
WATCH
- 当某个事务需要按条件执行时,就需要使用WATCH命令将给定的键设置为受监控。
- 返回值是一个简单的字符串,总是OK。
- 为redis事务踢动CAS(check-and-set)行为,可以监控一个或多个键,一旦有一个键被修改(或删除),之后的事务不会执行,监控一直持续到EXEC命令执行时。
Redis事件
基于事件
核心原理是基于事件的处理流程。
- 主程序处于一个阻塞状态的事件循环(event loop)中等待事件,当有事件发生时,根据事件的属性分发到相对应的处理函数中进行处理。事件是以并发的方式发送到服务处理器,服务处理器将事件整合到一个有序队列中,并发到具体的请求处理器进行处理。
- Redis程序都是围绕事件循环进行的。事件循环同时监控多个事件(Redis对于连接套接字的抽象),当套接字变为可读或者可写状态,则会触发该事件,把就绪的事件放在一个待处理事件的队列中,以有序、同步的方式发送给事件处理器进行处理。(Fire过程)
- Redis事件循环会保存两个表:events和fired列表,前者存储正在监听的事件,后者存储就绪的事件。
- Redis处理所有命令都是顺序执行的,包括客户端的连接请求、内部定时执行的任务等。所以当Redis处理一个复杂度高、时间很长的请求(如keys命令、自动删除一个过期的大key),则其他客户端连接有可能被阻塞。所以一般不使用大key,否则可能会造成业务卡顿。
Redis事件处理流程
- 加载配置
- 配置参数初始化:创建事件循环
- 循环事件注册一个可读事件,用于处理响应客户端请求
- 执行事件循环,等待连接和命令请求
- 注册事件,被eventLoop监听
- 读写操作需要执行的就绪事件
Redis持久化
RDB
- RDB是一个快照文件,是把redis内存存储的数据写到磁盘上,当redis实例宕机恢复数据的时候,方便从RDB的快照文件中恢复数据。
- RDB是二进制文件,保存的时候体积比较小,恢复快,但可能会丢失数据。
AOF
- AOF是追加文件,当redis操作写命令的时候,都会存储这个文件中,当redis实例宕机恢复数据的时候,会从这个文件中再执行一遍命令恢复数据。
- 通常在项目中使用AOF恢复数据,速度虽然慢,但丢数据的风险小,AOF文件可设置刷盘策略,如每秒批量写入一次命令。
Redis数据淘汰策略
概述
- 谈淘汰策略之前,我们知道redis有过期删除,根据TTL时间进行定期采样删除或惰性删除
- 有过期策略为何还要淘汰策略?因为过期策略不能够完全精准的全部删除数据,会存在key没有被删除的场景,所以需要内存淘汰策略进行兜底。
内存淘汰策略
noeviction
:当内存使用超过配置的时候会返回错误,不会驱逐任何键allkeys-lru
:加入键的时候,如果过限,首先通过LRU算法驱逐最久没有使用的键volatile-lru
:加入键的时候如果过限,首先从设置了过期时间的键集合中驱逐最久没有使用的键allkeys-random
:加入键的时候如果过限,从所有key随机删除volatile-random
:加入键的时候如果过限,从过期键的集合中随机驱逐volatile-ttl
:从配置了过期时间的键中驱逐马上就要过期的键volatile-lfu
:从所有配置了过期时间的键中驱逐使用频率最少的键allkeys-lfu
:从所有键中驱逐使用频率最少的键