redis的简单了解
什么是Redis
简述
- Redis(Remote Dictionary Server,远程字典服务),是内存高速缓存数据库,且提供数据的持久化
- Redis是键值(Key-Value)存储系统,使用C语言编写
- 支持丰富的数据类型,例如:String、List、Set、ZSet(SortSet)、Hash、Stream
- 常用于缓存、事件的发布与订阅、高速列表等场景
- Redis是单线程,指的是网络IO模型与键值对读写是单线程的,即由一个线程来完成。但Redis的其它功能是支持异步的(由额外的线程进行处理),比如:持久化、异步清除过期数据、集群数据同步等新版的Redis网络IO模型已经支持多线程并发处理
优点
- 读写性能优异
- Redis基于内存与高速缓存存储数据。可以达到读的速度是110000次/s,写的速度是81000次/s
- 支持ACID
- 原子性atomicity。Redis官方文档给的理解是,Redis的事务是原子性的:所有的命令,要么全部执行,要么全部不执行。而不是完全成功
- 一致性consistency。redis事务可以保证命令失败的情况下得以回滚,数据能恢复到没有执行之前的样子,是保证一致性的,除非redis进程意外终结
- 隔离性Isolation。redis事务是严格遵守隔离性的,原因是redis是单进程单线程模式(v6.0之前),可以保证命令执行过程中不会被其他客户端命令打断
- 持久性Durability。redis事务是不保证持久性的,这是因为redis持久化策略中不管是RDB还是AOF都是异步执行的,不保证持久性是出于对性能的考虑
- 数据类型丰富
- Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作
- 支持发布、订阅、通知
- Redis支持 publish/subscribe, 通知, key 过期等特性
- 集群
- 提供哨兵模式,增强了节点的高可用
- 提供了分片模式,用于支持服务水平拓展
事务
简述
- redis 事务的本质是一组命令的集合
- redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令
相关命令
- MULTI
- 开启事务,redis会将后续的命令逐个放入队列中,然后使用EXEC命令来原子化执行这个命令系列
- EXEC
- 执行事务中的所有操作命令
- DISCARD
- 取消事务,放弃执行事务块中的所有命令
- WATCH
- 监视一个或多个key,如果事务在执行前,这个key(或多个key)被其他命令修改,则事务被中断,不会执行事务中的任何命令
- UNWATCH
- 取消WATCH对所有key的监视
事务异常处理
- 语法错误,事务回滚(编译器错误)
- 类型错误,事务不回滚(运行时错误)
CAS(check-and-set)乐观锁
- WATCH 命令可以为 Redis 事务提供乐观锁行为
事务流程(CAS)
- WATCH(监视字段)
- MULTI(开启事务)
- 事务内命令列表
- EXEC(执行事务)
Redis事务其它实现方案
- lua脚本
发布与订阅模式
Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息
分类
- 基于频道(Channel)
- 发布消息。publish channel message
- 订阅频道。subscribe channel1 [channel2 ...]
- 取消订阅。unsubscribe channel
- 基于模式(Pattern)
- 订阅。psubscribe channel
- 取消订阅。punsubscribe channel
事件
Redis 采用事件驱动机制来处理大量的网络IO。它并没有使用 libevent 或者 libev 这样的成熟开源方案,而是自己实现一个非常简洁的事件驱动库
Redis中的事件驱动库只关注网络IO,以及定时器
文件事件
简述
- 用于处理 Redis 服务器和客户端之间的网络IO
- Redis基于Reactor模式开发了自己的网络事件处理器,也就是文件事件处理器。文件事件处理器使用IO多路复用技术,同时监听多个套接字,并为套接字关联不同的事件处理函数。当套接字的可读或者可写事件触发时,就会调用相应的事件处理函数
处理流程
- 客户端发起连接请求 --> 连接应答处理器(AcceptEvent)
- 客户端发起命令请求 --> 命令请求处理器(ReadEvent)
- 服务器发送命令回复(响应结果)--> 命令回复处理器(WriteEvent)
时间事件
简述
- Redis 服务器中的一些操作(比如serverCron函数),需要在给定的时间点执行,而时间事件就是处理这类定时操作的
- 时间事件都放在一个无序链表中,每当时间事件执行器运行时,它就遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器
分类
- 定时事件
- 让一段程序在指定的时间之后执行一次
- 返回值是 AE_NOMORE,那么这个事件是一个定时事件
- 该事件在达到后删除,之后不会再重复
- 周期性事件
- 让一段程序每隔指定时间就执行一次
- 返回值是非 AE_NOMORE 的值,那么这个事件为周期性事件
补充
aeEventLoop
aeEventLoop是整个事件驱动的核心,它管理着文件事件表和时间事件列表,不断地循环处理着就绪的文件事件和到期的时间事件
事件处理
aeMain函数以一个无限循环不断地调用aeProcessEvents函数来处理所有的事件
删除事件
当不在需要某个事件时,需要把事件删除掉
- aeDeleteEventLoop函数执行步骤
- 根据fd在未就绪表中查找到事件
- 取消该fd对应的相应事件标识符
- 调用aeApiFree函数,内核会将epoll监听红黑树上的相应事件监听取消
缓存管理
最大缓存设置
- 大容量缓存是能带来性能加速的收益,但是成本也会更高,而小容量缓存不一定就起不到加速访问的效果
- 系统的设计选择是一个权衡的过程,建议把缓存容量设置为总数据量的 15% 到 30%,兼顾访问性能和内存空间开销
- 设置命令:CONFIG SET maxmemory 4gb
缓存过期管理
缓存过期方式
被动过期(惰性删除)
- 数据到达过期时间,不做处理,只有访问这个键时才会检查它是否过期,如果过期则清除,返回不存在;如果未过期,返回数据
- 用存储空间换取处理器性能(拿空间换时间)
- 最大化地节约CPU资源,发现必须删除的时候才删除
- 如果大量过期键没有被访问,会一直占用大量内存,内存压力很大
定时删除
- 在设置某个key 的过期时间同时,为每个设置过期时间的key都创造一个定时器;当key过期时间到达时,由定时器任务立即执行对键的删除操作
- 用处理器性能换取存储空间(拿时间换空间)
- 该策略可以立即清除过期的键,节约内存,快速释放掉不必要的内存占用
- CPU压力很大,无论CPU此时负载量多高,均占用CPU,会影响redis服务器响应时间和指令吞吐量,占用大量的CPU资源去处理过期的数据
定期删除
- 每隔一段时间就对一些键进行检查,删除其中过期的键(周期性轮询redis库中的时效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删除频度)。该策略是惰性删除和定时删除的一个折中,既避免了占用大量CPU资源又避免了出现大量过期键不被清除占用内存的情况
- 周期性抽查存储空间 (随机抽查,重点抽查)
- CPU性能占用设置有峰值,检测频度可自定义设置
- 内存压力不是很大,长期占用内存的冷数据会被持续清理
- 难以确定删除操作执行的时长和频率
缓存过期设置
- noeviction
- 该策略是Redis的默认策略,一般生产环境不建议使用
- 在这种策略下,一旦缓存被写满了,再有写请求来时,Redis 不再提供服务,而是直接返回错误
- 这种策略不会淘汰数据,所以无法解决缓存污染问题
- volatile-random
- 在设置了过期时间的键值对中,进行随机删除
- 因为是随机删除,无法把不再访问的数据筛选出来,所以可能依然会存在缓存污染现象,无法解决缓存污染问题
- volatile-ttl
- TTL 数据淘汰机制中会先从过期时间的表中随机挑选几个键值对,取出其中 ttl 比较小的键值对淘汰
- volatile-lru
- 最近最少使用。长期未使用的数据,优先被淘汰
- volatile-lfu
- 最不经常使用。在一段时间内,使用次数最少的数据,优先被淘汰
- allkeys-random
- 筛选的数据范围是全部缓存,其它与volatile-random一样
- allkeys-lru
- 筛选的数据范围是全部缓存,其它与volatile-lru一样
- allkeys-lfu
- 筛选的数据范围是全部缓存,其它与volatile-lfu一样
缓存和数据库一致性分析
缓存流程
- 访问Redis读取数据
- 存在?
- 存在,返回结果
- 不存在
- 从数据库读取
- 存在,返回结果并将数据放入缓存中
- 不存在,返回空值
更新缓存的四种方式
Cache Aside Pattern(旁路缓存模式)
最常使用的模式
在并发读写场景下可能会出现数据不一致的问题(賍读)
存在首次请求数据一定不在cache的问题
- 读操作
- 从cache中读取数据,读取到就直接返回
- cache中读取不到的话,就从db中读取数据返回
- 再把db中读取到的数据放到cache中
- 写操作
- 先更新db
- 直接删除cache
Read/Write Through Pattern(读/写穿透模式)
Read/Write Through Pattern 中服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 db,从而减轻了应用程序的职责
这种缓存读写策略在平时在开发过程中非常少见
- 读操作
- 从cache中读取数据,读取到就直接返回
- 读取不到的话,先从db加载,写入到cache后返回响应
- 写操作
- 先查cache,cache中不存在,直接更新db
- cache中存在,则先更新cache,然后cache服务自己更新db(同步更新cache和db)
Write Behind Caching Pattern(异步缓存写入模式)
在更新数据的时候,只更新缓存,不更新数据库,而我们的缓存会异步地批量更新数据库。这个设计的好处就是让数据的I/O操作飞快无比(因为直接操作内存嘛 ),因为异步,write backg还可以合并对同一个数据的多次操作,所以性能的提高是相当可观的
由 cache 服务来负责 cache 和 db 的读写
这种策略在我们平时开发中比较少见,但是它在消息队列中消息的异步写入磁盘、MySQL 的 Innodb Buffer Pool 机制都用到了这种策略
- 写操作
- 先查cache,cache中不存在,异步批量更新db
- cache中存在,则先更新cache,然后cache服务自己更新db(异步更新db)
应用场景
热点数据的缓存
缓存规则
- 查询时缓存
- 在查询时从缓存中读取数据,未命中时,从数据库读取数据,并将该数据保存到缓存中。(设计时需要考虑缓存穿透的问题)
- 适用于对于数据实时性要求不是特别高的场景
- 保存或更新时缓存
- 在插入数据后,删除缓存中对应的数据,在下一次查询时进行缓存(或者主动进行缓存)
- 适用于字典表、数据量不大的数据存储
限时业务的运用
- 通过使用expire命令设置一个键的生存时间(可以通过ttl查看键的剩余时间),到时间后从缓存中移除
- 例如:短信验证码30秒,限时的优惠活动信息等业务场景
计数器相关问题
- 可以通过incrby命令进行原子性的递增,以达到计数效果
- 例如:秒杀活动、分布式序列号生成、接口访问频率限制(记录访问次数)、手机短信验证码生成频率限制(预防单个手机号频繁发送)
分布式锁
- 分布式锁,分为加锁和解锁。加锁需要保证原子校验和更新;解锁需要保证原子校验和删除
- 在Redis中setnx命令进行原子校验并设置缓存,以达到加锁的效果
延时操作
- 通过Redis的时效性Key,进而实现延迟消息的功效
排行榜相关问题
- 可以通过ZSet(SortSet)实现高性能的排行榜的功能
- 例如:热点数据排行榜、投票榜、实时统计榜
点赞、好友等相互关系的存储
- 在Redis集合相关的命令中,如:求交集、并集、差集之类的,可以快速的分析它们的关联信息
简单队列
- 可以在List数据结构中,通过pop、push实现简单队列