redis的简单了解

106 阅读12分钟

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)

  1. WATCH(监视字段)
  2. MULTI(开启事务)
  3. 事务内命令列表
  4. 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多路复用技术,同时监听多个套接字,并为套接字关联不同的事件处理函数。当套接字的可读或者可写事件触发时,就会调用相应的事件处理函数

处理流程

  1. 客户端发起连接请求 --> 连接应答处理器(AcceptEvent)
  2. 客户端发起命令请求 --> 命令请求处理器(ReadEvent)
  3. 服务器发送命令回复(响应结果)--> 命令回复处理器(WriteEvent)

时间事件

简述

  • Redis 服务器中的一些操作(比如serverCron函数),需要在给定的时间点执行,而时间事件就是处理这类定时操作的
  • 时间事件都放在一个无序链表中,每当时间事件执行器运行时,它就遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器

分类

  • 定时事件
    • 让一段程序在指定的时间之后执行一次
    • 返回值是 AE_NOMORE,那么这个事件是一个定时事件
    • 该事件在达到后删除,之后不会再重复
  • 周期性事件
    • 让一段程序每隔指定时间就执行一次
    • 返回值是非 AE_NOMORE 的值,那么这个事件为周期性事件

补充

aeEventLoop

aeEventLoop是整个事件驱动的核心,它管理着文件事件表和时间事件列表,不断地循环处理着就绪的文件事件和到期的时间事件

事件处理

aeMain函数以一个无限循环不断地调用aeProcessEvents函数来处理所有的事件

删除事件

当不在需要某个事件时,需要把事件删除掉

  • aeDeleteEventLoop函数执行步骤
    1. 根据fd在未就绪表中查找到事件
    2. 取消该fd对应的相应事件标识符
    3. 调用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一样

缓存和数据库一致性分析

缓存流程

  1. 访问Redis读取数据
  2. 存在?
    • 存在,返回结果
    • 不存在
  3. 从数据库读取
    • 存在,返回结果并将数据放入缓存中
    • 不存在,返回空值

更新缓存的四种方式

Cache Aside Pattern(旁路缓存模式)

最常使用的模式

在并发读写场景下可能会出现数据不一致的问题(賍读)

存在首次请求数据一定不在cache的问题

  • 读操作
    1. 从cache中读取数据,读取到就直接返回
    2. cache中读取不到的话,就从db中读取数据返回
    3. 再把db中读取到的数据放到cache中
  • 写操作
    1. 先更新db
    2. 直接删除cache
Read/Write Through Pattern(读/写穿透模式)

Read/Write Through Pattern 中服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 db,从而减轻了应用程序的职责

这种缓存读写策略在平时在开发过程中非常少见

  • 读操作
    1. 从cache中读取数据,读取到就直接返回
    2. 读取不到的话,先从db加载,写入到cache后返回响应
  • 写操作
    1. 先查cache,cache中不存在,直接更新db
    2. cache中存在,则先更新cache,然后cache服务自己更新db(同步更新cache和db)
Write Behind Caching Pattern(异步缓存写入模式)

在更新数据的时候,只更新缓存,不更新数据库,而我们的缓存会异步地批量更新数据库。这个设计的好处就是让数据的I/O操作飞快无比(因为直接操作内存嘛 ),因为异步,write backg还可以合并对同一个数据的多次操作,所以性能的提高是相当可观的

由 cache 服务来负责 cache 和 db 的读写

这种策略在我们平时开发中比较少见,但是它在消息队列中消息的异步写入磁盘、MySQL 的 Innodb Buffer Pool 机制都用到了这种策略

  • 写操作
    1. 先查cache,cache中不存在,异步批量更新db
    2. cache中存在,则先更新cache,然后cache服务自己更新db(异步更新db)

应用场景

热点数据的缓存

缓存规则

  • 查询时缓存
    • 在查询时从缓存中读取数据,未命中时,从数据库读取数据,并将该数据保存到缓存中。(设计时需要考虑缓存穿透的问题)
    • 适用于对于数据实时性要求不是特别高的场景
  • 保存或更新时缓存
    • 在插入数据后,删除缓存中对应的数据,在下一次查询时进行缓存(或者主动进行缓存)
    • 适用于字典表、数据量不大的数据存储

限时业务的运用

  • 通过使用expire命令设置一个键的生存时间(可以通过ttl查看键的剩余时间),到时间后从缓存中移除
  • 例如:短信验证码30秒,限时的优惠活动信息等业务场景

计数器相关问题

  • 可以通过incrby命令进行原子性的递增,以达到计数效果
  • 例如:秒杀活动、分布式序列号生成、接口访问频率限制(记录访问次数)、手机短信验证码生成频率限制(预防单个手机号频繁发送)

分布式锁

  • 分布式锁,分为加锁和解锁。加锁需要保证原子校验和更新;解锁需要保证原子校验和删除
  • 在Redis中setnx命令进行原子校验并设置缓存,以达到加锁的效果

延时操作

  • 通过Redis的时效性Key,进而实现延迟消息的功效

排行榜相关问题

  • 可以通过ZSet(SortSet)实现高性能的排行榜的功能
  • 例如:热点数据排行榜、投票榜、实时统计榜

点赞、好友等相互关系的存储

  • 在Redis集合相关的命令中,如:求交集、并集、差集之类的,可以快速的分析它们的关联信息

简单队列

  • 可以在List数据结构中,通过pop、push实现简单队列