Redis

1 NoSQL

  • 传统关系型数据库存储大文件、图片、视屏等数据的效率低下

  • Not only SQL;非关系型数据库;四大分类:键值对存储(Redis),文档型存储(MongoDB),列存储(HBase),图关系存储;特点:

    • 方便扩展:数据之间没有关系

    • 大数据量时高性能

    • 支持多样数据类型

    • 没有固定查询语言

2 Redis基本信息

2.1 基本信息

  • Remote Dictionary Server;开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存也可持久化的日志型、Key-Value数据库,并提供多种语言API的非关系型数据库

  • 单线程;基于内作,速度快;执行速率CPU>内存>硬盘,但多线程导致CPU上下文切换,而对于内存操作来说,在一个CPU上执行效率是最高的

  • 作用:

    • 做数据库:Redis会周期性把数据库状态(存储在内存)持久化(数据写入内存);内存中数据断电即失,因此持久化很重要

    • 做缓存:将存储数据库中的热数据放到Redis中,高速获取数据

    • 做消息中间件MQ:发布订阅系统

    • 地图信息分析

    • 计时器,计数器(浏览量)

2.2 Windows下安装

  • 下载安装包,解压安装

  • ping:测试连通性

  • redis-server.exe启动服务器端,默认端口号6379

  • redis-cli.exe启动客户端,连接服务器端

2.3 Linux下安装

  • 软件一般在opt目录下

  • tar -zxvf redis...gz解压

  • 基本环境配置:

    • gcc编译语言:yum install gcc-c++

    • 执行make

  • 默认安装路径/usr/local/bin

  • 将redis配置文件redis.config复制到安装路径下,以配置文件方式启动

    • redis默认不是后台启动;修改配置文件:daemonize no改为yes

  • 启动服务:redis-server ...config

  • 启动客户端测试连接:redis-cli -p 6379;ping测试连接

  • 查看进程是否开启:ps -ef|grep redis

  • 关闭服务:shutdown

2.4 redis-benchmark

  • 压力测试工具,如并发连接数

  • redis-benchmark + 命令;控制台输出测试情形

  • 根据测试情形分析性能

3 数据结构

3.1 字符串 

  • 一个键最大能存储512MB;底层实现基于SDS即smpile dynamic string简单动态字符串
  • sdshdr结构源码定义(Redis使用C语言):
    struct sdshdr {
        int len;
        int free;
        char buf[];
    }

  • buf:字节数组,保存二进制数据而不是字符;最终的字符串同Java中的字符串,末尾为'\0'空字符,如"redis"存储为'r''e''d''i''s''\0';计算长度需要遍历字符直到遇到'\0',操作复杂度O(n);字符串长度为n,底层用于存储的数组长度为n+1
  • len:记录buf数组已使用字节数,也即SDS所保存的字符串长度
  • free:未使用空间,记录buf数组中未使用的字节数
  • SDS
    • len记录了SDS的长度,操作复杂度O(1);属性值的设置和更新由SDS的API自动执行;Redis的键也是这种字符串;所以strle命令操作性能极高,即使再长的字符串也无性能影响;传统字符串由于没有记录len,在拼接append操作前,需要先通过内存重新分配来扩展空间,否则很可能造成内存/缓冲区溢出;在缩短如截取trim操作后,也需要内存重新配分来释放未使用的空间,否则造成内存泄露;内存重分配涉及复杂算法,并且可能需要执行系统调用,它是一个耗时操作
    • Redis作为数据库,用于速度要求高、数据频繁修改的场合,就不能不在乎和解决内存重分配问题;于是使用SDS的free解除了字符串长度和底层保存字符串的数组长度之间的关联,即不一定还是n+1的关系;SDS API在对SDS修改前先进行内存空间检查,不足则自动扩容至所需的大小并设置free值,再进行修改
      • 空间预分配:即设置free值;设置规则为:对SDS修改后,若len值小于1MB,则free值等于len值,如修改后len=3,则分配free为3,数组长度为3+3+1;若len值大于等于1MB,则free始终设置为1MB;空间预分配策略减少了字符串追加操作时的内存重分配次数即避免频繁的动态扩充;如"redis"可以存储为'r''e''d''i''s''\0'' '' '' ';即有了free值,下一次重分配需要等到free空间使用完之后;次数从原来的N次追加操作时必定N次内存重分配降低为最多N次;这样更能满足Redis对字符串的安全性、效率和功能要求
      • 惰性空间释放:字符串缩短时,不释放减少的字符的内存,而是将它们设置为free保留,供未来使用;这个策略避免了内存重分配,还给未来字符串追加操作提供了优化;SDS还提供了在真正需要释放free时的API,防止此策略带来的空间浪费
    • 二进制安全:
      • SDS API是二进制安全的,即它会以处理二进制的方式处理buf存储的数据,程序不会对这个数据做限制和过滤;通过len属性而非空字符来判断最终字符串,因此Redis可以存储能转为二进制的任意类型数据;同时buf还遵循空字符结尾的惯例,即兼容传统字符串,使得SDS可以使用部分传统字符串定义的函数而不必重复编写函数
      • 传统字符串需合某种编码,且串中最多只能包含末尾的空字符,否则第一次遇到空字符时就过滤掉了后面要的数据;这些限制使得字符串只能保存文本数据,不能存储图片、音频、视频、压缩文件等二进制数据
    • SDS常用API:

    • 总结为:常数级复杂度获取字符串长度;避免缓冲区溢出;减少修改字符串时所需的内冲重分配;二进制安全;兼容部分原始字符串

3.2 List链表

  • 链表节点源码listNode:prev和next指针组成双端链表
    typedef struct listNode {
        struct listNode * prev;
        struct listNode * next;
        void * value;
    }
  • 链表源码list:带有表头节点、表尾节点、链表长度信息等;表头节点的前置节点和表尾节点的后置节点指向null,所以list也是无环链表;通过设置不同的类型特定函数,为链表存储不同类型数据 

3.3 Map字典

  • 又称符号表、关联数组、映射,用于保存键值对;底层以哈希表实现

3.4 跳跃表skiplist

3.5 整数集合intset

3.6 压缩列表ziplist

4 数据类型/对象

4.1 redisObject

  • Redis没有直接使用上面的数据结构来实现数据库,而是基于它们创建对象系统;数据库中,键值对key-value是一个对象;而键key总是一个字符串对象,值value可以为字符串、列表、哈希、集合、有序集合等数据类型对象
  • 对象用redisObject结构表示,主要包含了:
    • type:数据/对象类型,对应redis_string,redis_list,redis_hash,redis_set,redis_zset;键总是redis_string类型,而说”列表键“表示value是列表;使用type key命令得到value的数据类型
    • encoding:对象使用的编码即该对象的底层数据结构;每种类型的对象至少使用了两种不同编码;使用object encoding key查看编码;为对象设置和修改编码以提高灵活性和效率,如当列表对象元素较少时底层使用ziplist,它比linkedlist更节约内存,在元素较少时连续存储,能快速被载入内存,但元素较多时linkedlist更适合保存且功能更强大
      编码常量编码对应的底层数据结构
      REDIS_ENCODING_INTlong类型整数
      REDIS_ENCODING_EMBSTRembstr编码的简单动态字符串
      REDIS_ENCODING_RAW简单动态字符串
      REDIS_ENCODING_HT字典(hashtable)
      REDIS_ENCODING_LINKEDLIST双端链表
      REDIS_ENCODING_ZIPLIST压缩列表
      REDIS_ENCODING_INTSET整数集合
      REDIS_ENCODING_SKIPLIST跳跃表和字典
    • ptr:指向对象的底层数据结构实现 ,如字符串对象时指向SDS

4.2 字符串对象

  • 字符串可用int,embstr,row编码;整数值用int,浮点数或字符串值且长度小于等于32字节时用embstr,否则用row;embstr调用一次内存分配函数分配一段连续空间来存储reidsObject和sdshdr,空间连续能更好的实现缓存,而row调用两次并分配两块空间来分别存储;释放内存时也对应调用一次或两次;int和embstr经过操作变化后不在其表示范围内了,则自动转为row编码
  • 字符串命令的实现

4.3 列表对象

  • 列表可用ziplist,linkedlist编码
  • ziplist编码的压缩列表,每个压缩节点entry保存了列表元素,类似字符串数组存储;只能用于所有字符串元素的长度小于64字节且元素数量少于512的,但配置文件中可以修改这个条件;否则转为linkedlist
  • linkedlist编码的双端链表,每个链表节点保存列表元素,这个元素只能是字符串对象
  • 列表命令的实现

4.4 哈希对象

  • 哈希对象可用ziplist,hashtable编码
  • ziplist编码先将保存了key的压缩列表节点推入列表表尾,再将保存了value的的压缩列表节点推入表尾,即同一个键值对的两个节点一前一后且总紧挨在一起;用于所有键值对的键和值的字符串长度小于64字节且键值对数量少于512个,否则编码转为hashtable;条件可修改
  • hashtable编码,则保存key列表,每个key指向自己的value
  • 哈希命令的实现:

4.5 集合对象

  • 集合对象可用intset,hashtable编码
  • intset编码保存所有元素是整数的;不是整数或者元素数量大于512时转为hashtable
  • 集合命令的实现:

4.6 有序集合对象

  • 有序集合可用ziplist,skiplist编码
  • 详解看书《Redis设计与实现》
  • 有序集合命令的实现

4.7 命令

  • 命令不缺分大小写;key区分大小写;其他命令查看官网;或help + @类型查看

  • 一些命令可以针对任意类型执行,如del,expire,rename,type,object等;而各种对象命令的实现只能针对对应的类型执行,否则Redis将返回一个类型错误;为了确保正确执行,Redis在命令执行前会执行类型检查,即验证redisObject中的type属性;还会进行多态命令校验,即根据对象的实际编码方式选择对应的命令

5 内存共享与回收

  • Redis在对象系统中构建了引用计数技术;即redisObject中的refcount属性
  • 引用计数技术实现内存共享:如果新建的对象在内存中已经存在,为了节省内存,直接将其指针指向已有的值对象,refcount值加1;Redis服务器默认会初始0-9999之间的整数字符串,要用到的时候直接共享;但共享机制不包含值对象太复杂的如链表对象,因为作为共享对象,要验证是否和目标对象是否一致,值对象是整数字符串时验证操作复杂度O(1),其他字符串时是O(n),这两种适合做共享;受CPU限制,其他类型对象不进行共享
  • 引用计数技术实现GC:跟踪引用计数信息,在适当时候自动GC:创建对象时,refcount初始化为1;被新程序使用时incrRefcount函数使其加1;不被使用时decrRefCount函数使其减1;当值为0时,对象所占内存被释放
  • lru:空转时长,redisObject的属性,表示对象最后一次被命令访问的时间;由object idletime key查看;当服务器打开maxmemory,并且使用的回收算法是volatile-lru或allkeys-lru时,如果内存占用超过maxmemory所设值,则lru值较高的对象优先释放回收

6 Redis数据库

6.1 服务器与客户端

  • redisServer即服务器状态,db数组属性保存了所有数据库,dbnum属性表示DB数量,由服务器的database选项配置,默认16,服务器以此创建数据库

  • redisClient即客户端状态,db指针指向redisServer.db数组的一个元素,即记录客户端当前目标数据库信息

  • select 0-15:切换数据库即修改redisClient.db指针的指向;默认0号数据库

  • dbsize:查看数据库大小

  • flushdb:清空数据;flushall:清除所有库数据

6.2 键空间

  • 数据库由redisDb结构表示,属性dict字典称为键空间,保存所有键值对;简单理解为向数据库中添加键值对,dict保存了键列表,每一个键又指向各自的value
  • 增删改查操作都是在键空间中操作,如添加键值对时,dict中添加一个键即一个stringObject对象;这个键指向值即任意Redis对象
  • 其他操作:flushdb清空整个数据库也即通过删除键空间中所有键值对;randomkey随机获取某个键;dbsize统计键值对数量;exsits,rename,keys等......
  • Redis在读写键空间时的维护操作:
    • 读取键后(读操作和写操作都要读取键),服务器根据判断键是否存在,来更新键空间命中次数hit或不命中次数miss,即info status命令中的keyspace_hits和keyspace_misses属性值
    • 读取键后,更新键的lru值
    • 读取键时,如果发现键过期,则先删除过期键,在执行其他操作
    • 客户端使用watch监听某个键,服务器又对这个键做修改,则此键被标记为脏dirty,从而引起事务的注意;每修改依次,针对脏键的计数器值加1,计数器会触发持久化和复制操作

6.3 键的生存与过期

  • expire或pexpire key 时间:以秒或毫秒设置键生存时间(time to live,TTL);生存时间过后服务器自动删除该键
  • expireat或pexpireat命令可设置键过期时间setex命令可以设置字符串键的同时为键设置过期时间;过期时间是一个unix时间戳,到达该时间戳时服务器自动删除该键
  • TTL或PTTL key命名获取该键的剩余生存时间:过期时间减去当前时间的时间差
  • redisDb结构中的expires字典属性保存了所有键的过期时间;生存时间和过期时间设置的命令最终转成pexpireat命令,执行此命令时服务器就在过期字典中关联给定的键和时间
  • persist key命令移除键的过期时间,是pexpireat命令的反向操作
  • 判断键是否过期:检查键是否存在于过期字典,如果存在则可获取过期时间;is_expired(key)检查当前unix时间戳是否大于过期时间,大于则过期,否则未过期,或直接获取生存时间来判断
  • 过期键的删除策略:
    • 定时删除:设置过期时间的同时创建定时器timer,让timer在过期时间来临时立即执行删除键操作;这能尽快释放内存,但当过期键较多时或占用较长CPU时间,影响吞吐量;因此需要看情况即内存紧张还是CPU紧张;Redis服务器不使用这种策略
    • 惰性删除:每次从键空间获取键时,检查键是否过期,过期则删除,否则返回该键;这对CPU时间友好即只在必要的情况下进行且只对当前操作的键执行而不浪费时间关系无关的键;缺点是过期键可能一直存在数据库中,占据了内存;甚至可能永远访问不到某些键以致于它永远不被删除(除非手动flushdb),这可视为内存泄漏
      • 所有读写命令执行前都会调用expireIfNeeded函数,对键进行检查,若过期则删除,否则不做动作;这避免了命令触碰到过期键;所以每个命令的实现都需要能处理键存在和不存在这两种情况
    • 定期删除:也即被动删除;每隔一段时间,程序对数据库进行检查,删除过期键;这整合折中了定时删除和惰性删除,需要平衡好删除操作执行的时长和频率
      • 服务器周期性调用serverCron函数,进而调用activeExpireCycle函数来执行定期删除,它会从一定量的数据库中获取一定量的随机的键进行检查和删除;全局变量current_db记录检查进度,在下一次函数调用时接着上一次的进度后面进行;所有数据库都被检查一遍后值归0

7 事务

  • Redis事务是一种将多个命令打包,然后一次性、顺序执行多个命令的机制;事务具有原子性(不会中断去执行其他命令)、一致性、在某种持久化模式下的永久性、隔离性;但区别于关系型数据库事务,Redis事务没有回滚机制(Redis追求简单高效),即使事务中的某个命令出现错误也会继续执行直到所有命令执行完毕

  • 开启:multi

    • 这让客户端从非事务状态转为事务状态,即flags属性中打开REDIS_MULTI;非事务状态下命令会立即执行;事务状态下,除了exec,discard,watch,multi命令会立即执行,其他命令都放入事务队列,不立即执行,向客户端返回queued回复

    • 事务队列:存储在mstats事务状态属性中,包含了事务队列(数组)和计数器;数组元素是每个入队命令的相关信息

  • 命令入队:这可能是事务出错的地方;Redis通过错误检测来保证一致性

    • 命令入队过程中,命令不存在或格式错误(类似编译时异常),则拒绝执行事务

    • 事务执行过程中发生错误(如使用了类型不匹配的命令,类似运行时异常),事务依然继续执行后面的命令

    • 事务执行过程中停机,不会影响一致性,因为可以根据持久化模式下的文件恢复数据

  • 提交执行:exec;遍历客户端中所有事务队列,执行保存的命令并返回结果

    • 运行时发生异常,则发生异常的命令不执行,其他命令正常执行;区别于数据库的事务回滚

  • 取消事务:discard;放弃事务后队列中的命令全部移除

  • 监控:watch key;乐观锁;在exec执行前,可用于监听key,如果被监听的key至少有一个被修改了,则服务器拒绝执行事务并返回代表执行失败的空回复给客户端

    • watched_keys字典,它的key保存了被监听的key,value是所有监听了key的客户端链表;因此很容易直到哪些key正在被监听及监听的客户端

    • 所有修改命令如set、flashdb等,执行后都会调用touchWatchKey函数对watched_keys进行检查,看是否有客户端在监听刚才被命令修改的key,如果有,该客户端的REDIS_DIRTY_CAS标识打开,表示事务安全性已被破坏;以此判断事务是否安全后再决定是否执行事务

    • 放弃监视:unwatch;然后再重新监控,即快速失败-获取新值-重试机制

8 配置

  • redis.config:

    • unit:单位配置

    • include:包含其他配置文件

    • 网络设置:

      • bind:ip绑定,默认本机

      • protecte-mode yes:保护模式

      • port 6379:端口

    • 通用:

      • daemonize yes:以守护线程方式运行

      • pidfile /var/run/redis_6379.pid:如果以后台方式运行,则需要指定一个pid文件

      • loglevel:日志级别;默认notice

      • logfile:日志输出位置

      • databases 16:数据库数量

      • always-show-logo yes:logo显示

    • 快照:持久化规则,默认使用rdb

      • save 900 1:15分钟内至少有一个key被修改,则持久化

      • stop-writes-on-bgsave-error yes:当持久化出错时,是否继续工作

      • rdbcompression yes:是否压缩rdb文件

      • rdbchecksum yes:保存rdb文件时是否错误检查

      • dir ./:文件保存目录

    • Replication:主从复制设置

    • Security:安全设置,如Redis默认无密码

    • Clients:客户端设置,如连接数,内存满之后的处理策略

    • Appendonly :持久化使用aof配置

9 持久化

  • Redis数据库基于内存,如果不将内存中数据状态保存到磁盘,故障时数据丢失;Redis单独创建一个线程在后台异步做持久化工作

9.1 RDB

  • Redis DataBase;文件结构:

    • 最开头"REDIS"5个字符常量(的二进制),用于快速检查载入的文件是否RDB文件

    • db_version:4字节整数字符串,标识Redis版本

    • databases:任意个数据库

      • SELECTDB:1字节常量,标识将要读取数据库号码

      • db_number:1,2或5字节数据库号码,根据号码切换数据库,为将要读入的键值对寻找正确的数据库

      • key_value_pairs:数据库中的键值对,包含数量,类型type,键key,内容value,是否有过期时间expiretime_ms,过期时间ms等属性;type为数据类型或底层编码,用来决定如何读入或解释value

      • value的编码:。。。。。。

    • EOF常量,标识RDB文件正文内容的结束,即键值对载入完毕

    • check_sum:无符号8字节整数,是通过对前面4个部分的计算得出的校验和;载入时计算的校验和与此值对比,来检查RDB文件是否出错或损坏

  • 分析RDB文件:od -c dump.rdb:命令用于打印rdb文件,可看到文件结构;或者使用RDB文件检查工具redis-check-dump,或其他处理RDB文件的工具

  • 创建文件:在指定时间范围内,将内存中数据快照写入磁盘

    • 先将数据写到临时rbd文件(一个经过压缩的二进制文件),持久化数据,然后将临时文件替换上次持久化好的文件;主线程不参与任何IO操作,保证了高效;针对大规模数据的恢复,RDB比AOF高效,缺点是可能丢失最后一次持久化后的数据

    • 数据保存默认保存文件dump.rdb;在save配置条件达成,或执行save或bgsave命令,或执行flushall命名,或退出redis时,都自动触发rdb规则即产生文件;但会先进行键检查,过期键不被保存到文件中

    • save命令会阻塞服务器进程直到RDB文件创建完成,即期间拒绝其他命令的执行

    • bgsave命令会派生出子进程去负责创建文件,服务器进程可以继续执行其他命令;期间还可执行其他命令;但不执行另外的save和bgsave命令以防止父进程和子进程同时执行两个rdbsave调用而出现竞争条件;bgrewriteaof也是创建子进程去执行,和bgsave命令不能同时执行,因为创建两个进程去同时执行磁盘写操作不是个好性能;如果bgsave正在执行,则bgrewriteaof延迟执行;如果bgrewriteaof正在执行,则拒绝执行bgsave

    • 自动间隔性保存:配置服务器的配置文件的save选项,或传入启动参数,让服务器定期自动执行bgsave;配置条件可多个,只要任意一个条件成立后都能触发执行;如默认的save 900 1,save 300 1,save 60 1000 配置条件,表示服务器在900秒内对数据库进行了至少1次修改;服务器用保存条件初始化redisServer的saveParams属性值,这是保存saveParam对象的数组,对象拥有seconds秒数和changes修改数属性;周期性函数serverCron默认每隔100毫秒执行一次,用来对正在执行的服务器进行维护,工作之一是检查save选项的保存条件是否达成,达成则执行bgsave

    • redisServer的其他属性:dirty计数器,记录距上一次成功执行save或bgsave命令后,服务器对数据库进行了多少次修改;lastsave属性记录上一次成功执行save或bgsave命令时的时间戳

  • 恢复数据:数据恢复时直接将快照文件读取到内存以恢复数据库状态;服务启动时RDBb文件自动载入,检查文件并恢复数据,可看到打印日志DB loaded from disk;由函数rdbload完成;若以主服务器模式运行,则会检查文件中的键,过期键不被载入;若以从服务器模式运行,则会保存所有键,不过在主服务器同步数据时会清空从服务器数据,过期键对服务器也没影响

9.2 AOF

  • append only file;追加文件,即保存所有写操作命令来记录数据库状态,而不像RDB去保存键值对;命令以命令请求协议格式保存;服务器启动时载入和执行AOF文件中的命令来恢复数据库状态

  • 如果服务开启了AOF持久化功能,则优先级较RDB高,因为AOF文件更新频率比RDB的快;不开启AOF才使用RDB

  • AOF持久化过程:

    • 命令追加:命令(协议格式)追加到redisServer的aof_buf缓冲区末尾,

    • 文件写入和同步:每次结束一个事件循环(发送命令,收到恢复)前,调用flushAppendOnlyFile函数,考虑将aof_buf中的内容写入到AOF文件;参数值可由appendfsync选项指定:always表示写入并同步,最安全也最低效,默认everysec表示写入文件并判断距离上次同步的时间是否超过1秒,超过则同步,no表示写入但不同步,同步时机由系统决定,最高效也最不安全;为了提高写效率,系统通常设计成将数据暂时存放在一个内存缓冲区,等待存满或到时间后一次性写入磁盘;这同时带来数据安全问题即可能数据丢失;系统提供fsync和fdatasync两个同步函数,强制将缓冲区数据刷到磁盘

    • 引入AOF做持久化且默认everysec即每秒将缓冲区指令同步到AOF文件,存储数据量较少、数据准确性较高、性能较高、数据丢失的风险也只是一秒内的数据,避免了使用RDB的存储的数据量较大、IO性能较低、额外进程带来内存消耗、数据丢失的风险

  • 、AOF文件载入:创建不带网络连接(命令直接来自文件而不是网络连接传输)的伪客户端(命令只能在客户端上下文中执行),执行从文件中分析和读取写命令,直到完成还原数据库状态

  • AOF文件重写:解决AOF文件越来越庞大的问题

    • 创建新AOF文件代替老AOF,新AOF不包含任何浪费空间的冗余命令,如SADD 同一个key的多条命令,合并成一条命令;所以需要先从数据库读取key对应的所有value,然后构建这一条命令;重写由aof_rewrite函数执行

    • aof_rewrite执行时会长时间阻塞线程,所以开启子进程来操作;但同时父进程可能修改数据,造成与重写后的AOF存储的数据不一致;于是设置AOF重写缓冲区,即写命令会被同时追加到AOF缓冲区和AOF重写缓冲区;AOF缓冲区正常写入和同步到AOF文件;在子进程完成重写工作后,通知父进程,父进程调用信号处理函数,将AOF缓冲区所有内容写入新AOF文件,重命名并覆盖老AOF文件

  • 服务器以AOF模式运行时,存在过期键且未被删除,则无影响;若被删除,则AOF文件末尾追加del命令显式记录该键被删除;bgrewriteaof命令执行AOF重写时,过期键不被保存到文件

10 事件

  • Redis服务器是事件驱动程序,需要处理两类事件:

10.1 文件事件

  • 服务器与客户端通过套接字进行连接和通信;Redis对套接字的操作的抽象即为文件事件

10.2 时间事件

  • Redis对需要在给定时间点执行的操作的抽象

11 客户端

12 服务器

13 主从复制

  • 服务器运行在复制模式下,从服务器的过期键的删除操作由主服务器来控制,即主服务器删除过期键的同时,显式向从服务器发送del命令以告知删除了该键,从服务器仅在接收到此命令后才删除键,否则即使遇到过期键也当作未过期键处理;这种统一的、中心化的删除策略保证了主从服务器的数据一致性

  • 将一台Redis服务器(主节点master/leader)的数据复制到多台服务器(从节点slave/follewer);数组单向复制即从主到从;主节点以写服务为主,从节点以读服务为主且只能读,即负载均衡;数据库状态达到一致,即主服务器的修改会同步到从服务器

  • 配置从机:slaveof hostIP port,在当前机器上配置连接并配置主服务器hostIP;这种配置是暂时的;永久配置需要在配置文件中配置slaveof选项;slaveof是异步指令,客户端发送请求后,从服务器先绑定redisServer的masterhost和masterport属性,并回复客户端OK,然后开始真正的执行复制工作

  • 建立套接字连接:根据属性值,创建连向master的套接字;套接字包含了用于处理复制工作的文件事件处理器;slave此时看作master的一个客户端;slave向master发送PING检查套接字的读写状态是否正常,以致是否能正确通信和处理命令请求;PING后如果master回复一个错误,或slave不能及时读取master的回复,则网络连接状态不佳,需重新建立套接字;如果获取到PONG回复则表示连接正常

  • 身份验证:获取PONG后,如果slave设置了masterauth属性,则需要身份验证,即向master发送参数为masterauth值的auth命令;master如果没有设置requirepass属性且slave没有masterauth属性,或者都设置后该属性值等同于auth命令发送的密码,则正常继续执行,否则返回invalid password;如果master设置requirepass而slave没有masterauth则返回NOAUTH;如果master没有设置requirepass而slave设置了masterauth则返回no password is set错误;所有错误都表示验证失败,导致不能正常执行,需要重建套接字或者slave放弃复制;验证通过,slave发送replconf listening-port portNumber即监听端口号,master设置slave_listening_port属性;info replication命令中展示这些状态和属性值

  • 复制的过程:

    • 同步:从服务器的数据库状态更新为主服务器状态

      • 从服务器先向主服务器发送sync命令;收到命令的主服务器执行bgsave在后台生成RDB文件,并用一个缓冲区记录从此开始执行的所有写命令

      • RDB文件传和缓冲区送给从服务器,载入后将数据库状态更新至主服务器执行bgsave时的状态;执行缓冲区中的写命令,将状态更新为主服务器当前状态

    • 命令传播:同步执行完后,状态一致;但一致性可能不会一直保持;如主服务器又执行了删除某个键;此时需要将此操作命令传给从服务器去执行,好再次保持一致;期间slave默认每秒一次的向master发送replconf ack [replication_offset]实现心跳检测,主要完成检测主从间的连接状态(master超过1秒未收到此命令则认为连接故障)、辅助实现min-slaves配置选项(min-slaves-to-write即slave个数和min-slaves-max-lag即slave的延迟值lag两个选项防止master在不安全的情况下执行写命令)、检测命令丢失(offset值不一致)

  • 旧版复制缺陷:主从处于一致状态;当主从服务器连接断开,而主服务器依然在做操作,从服务器尝试断线重连;连上后为了与主服务器保持一致,需要做复制;但此时同步的RDB文件明显包含了断线之前已经同步过的数据,而这是不必要的,只需要同步断线期间内主服务器修改的数据;sync操作需要生成文件、IO传输、网络传输、文件载入解析,是一个非常耗资源的操作,应让其在正真需要的时候才执行

  • 新版复制:psync代替sync做同步;两种模式:

    • 完整重同步:用于处理初次复制的情况,同sync

    • 部分重同步:处理断线后重复制的情况

      • 复制偏移量offset:双方服务器各自维护一个偏移量;每传播N字节数据和接收到N字节数据,复制偏移量+N;通过对比主从服务器的复制偏移量,很容易直到是否状态一致

      • 复制积压缓冲区:主服务器维护的默认1M大小的固定长度的FIFO队列(固定表示不会扩容,当元素大于队列长度时,队首元素被弹出);执行命令传播的同时,把写命令入队到复制积压缓冲区,即保存了最近传播的写命令及对应的数据的复制偏移量;断线重连后,从服务器将自己的offset传到master,由它决定:当offset之后的数据仍然存在于复制积压缓冲区中,则执行部分重同步,即将缓冲区中的那部分数据发送给slave,否则执行完成重同步

      • 服务器运行id:首次复制,slave会保存master的id,用于后续断线重连时校验是否是自己原来的master,是则执行部分重同步,否则执行完成重同步

  • psync命令:首次主从复制(包括执行过slaveof no one后),会主动发送psync ? -1进行完整重复制;否则发送psync runId offset到上一次复制的主服务器请求复制,主服务器决定执行何种复制;如果主服务器返回+FULLRESYNC runId offset回复,则执行完整复制;回复+CONTINUE则执行部分重同步,回复-ERR表示版本低于2.8,不识别psync命令,执行完整重同步

  • 一个主派生多个从,一个从只有一个主

  • 主出现故障,可由从提供服务,并实现快速的故障恢复

14 Sentinel哨兵

  • Redis对高可用的解决方案:由一个或多个Sentine组成Sentine系统,监听任意个master和slave,并在master下线时提升某个slave为master;master重新上线后成为slave
  • 启动Sentinel:redis-sentinel /...path/sentinel.conf或redis-server /...path/sentinel.conf --sentinel命令:
  • 初始化服务器:区别于初始化普通服务器,它不需要载入RDB等文件还原数据库状态
  • 将普通Redis服务器代码替换为Sentinel专用代码:某些命令不能使用
  • 初始化Sentinel状态:sentinel结构的属性填充,如保存所有监听的master实例信息的masters字典
  • 根据指定的配置文件初始化监听的服务器列表:所有master的初始化
  • 创建连向master的网络连接:两个异步连接,一个是命令连接,用于向master发送和接收命令;一个订阅连接,定于master的_sentinel_:hello频道
  • Sentinel默认以10秒每次的频率向master发送info命令,以获取master信息及拥有的slaves连接信息,以此创建slaves实例和命令连接及订阅连接,并以相同频率向slave发送info以获取信息
  • Sentinel默认以2秒每次的频率通过命令连接向所有被监视的服务器发送信息,如publish _sentinel_:hello 参数列表命令向服务器的该频道发送信息;参数以s_开头的是Sentinel的信息,以m_开头的是master的信息;Sentinel通过订阅连接发送命令以获取被监听的服务器信息,如subscribe _sentinel_:hello;监听了同一个服务器的所有Sentinel之间信息共享,即互相能收到信息,自己收到自己发送的信息时不做任何处理,收到其他Sentinel发送的信息时更新字典中的结构实例;当发现新的Sentinel时,双向建立命令连接,最终形成连接网络;所以不需要Sentinel的地址,同一个网络中各Sentinel互相自动发现和通信;它们之间不需要建立订阅连接,因为更新数据需要依赖监听的服务器
  • Sentinels字典:实例结构中的该字典属性保存了除自己外的监听了相同服务器的所有Sentinel,键为以ip:port格式命名的SentinelName,用于判断是否同一个Sentinel;值为Sentinel实例结构
  • 主观下线:Sentinel默认1秒每次的频率向所有与它建立了命令连接的实例发送PING命令,以判断实例是否在线;回复如果不是实例+PONG、-LOADING、-MASTERDOWN的任意一种,或超时未收到回复,则认为下线,将结构中flag属性的SRI_S_DOWN标识打开;Sentinel配置文件中down-after-milliseconds选项指定超时时间
  • 客观下线:当Sentinel将master判断为主观下线后,会询问其他Sentinel是否也同样认为,询问命令SENTINEL is master-down-by-[master信息],收到此命令的Sentinel解析参数信息去检查在线状态,然后返回包括down-state等三个参数回复;当足够多Sentinel都做出下线判断时,master判定为客观下线,执行故障转移操作;决定数量可在配置中设置quorum选项;各个Sentinel判断master主观或客观下线的条件可能不同
  • 选举领头Sentinel:master被判为客观下线后,所有Sentinel协商选举领头Sentinel,并由它对master做故障转移;选举规则:
    • 所有在线Sentinel都有资格参选
    • 每个Sentinel的配置纪元属性会将其他Sentinel设置为领头,不能二次修改;即投票
    • 每个发现master下线的Sentinel都会要求其他Sentinel将票投给自己,即再次发送的命令SENTINEL is master-down-by-携带的参数中包括自己的runid;以先到先得原则设置,后来者被拒绝;还未设置的设置后回复要求来源,源Sentinel以此统计自己的票数
    • 最终某个Sentinel的票数超出参与者的半数,则被选举为领头;给定时间内未选出,一段时间后再次选举直到选出领头
  • 故障转移:
    • 从所有slave中选出一个并转为master:领头将所有slave保存到列表,删除下线的、断线的、最近5秒没有回复领头info的、与master断开超过某个值的,最终筛选出数据完整,状态良好的,在根据优先级排序(如果优先级相同,再根据偏移量排序得到最大的,如果还相同,根据runid排序选出最小的),执行slaveof no one;命令执行后,以1秒每次的频率向升级的服务器发送info以获取回复中role,当role从slave变为master后说明升级完成
    • 让所有slave改为复制新的master:其他slave执行slaveof 新masterIP port
    • 将已下线的master转为slave:同上,认主

15 集群

  • Redis的分布式数据库解决方案;通过分片进行数据共享,提供复制和故障转移
  • 各个独立的node连接起来构成集群;命令CLUSTER MEET ip port,成为握手handshake,握手成功即node被加入集群;CLUSTER NODES查看集群中的节点
  • 节点启动:服务器启动时查看cluster-enabled选项,yes打开时表示开启集群模式;节点依然能够使用单机模式下的服务组件(各种命令和函数)和用redisClient结构保存客户端状态;而在集群模式下才能使用的,保存到:
    • clusterNode:集群结构,包含了节点信息如状态、名称、IP、套接字、输入输出缓冲区}(用来连接客户端)等,一个clusterLink,一个clusterState,一个slots数组和numslot属性记录自己负责的slot信息,广播时直接发送整个数组而不用去整个slots中查找
    • clusterLink:保存了用来连接其他节点所需的信息,如套接字、输入和输出缓冲区
    • clusterState:保存了在当前节点视角下的集群的信息,如在线状态、包含的节点数、配置纪元(用于故障转移)、slots等,slots记录了集群中所有槽的指派信息,值为null表示为分配,否则值指向一个clusterNode;全局记录槽指派信息避免了去遍历所有clusterNode才能获取信息,复杂度从O(N)降低为O(1);因此两个slots都有存在的必要
  • 握手的实现:节点A与B握手,都创建clusterNode来保存对方的信息;最后A通过Gossip协议把B的信息广播给所有节点,让它们去和B握手
  • 槽指派:集群的整个数据库被分为16384个槽slot(分片);数据库中的键属于任意一个slot;节点可以负责处理任意个slot;当所有slot都有节点在处理时,集群处于上线状态ok,否则为下线状态fail;向节点发送CLUSTER ADDSLOT [slot编号]即为节点指派负责的slot;在slots二进制数组中,索引i上对应值为1,表示负责第i个slot,为0表示不负责;槽指派即设置两个slots数组的值,复杂度O(1);numslot统计值为1的个数;节点会将这些槽指派信息在集群中广播
  • 集群中执行命令:线上的集群,在客户端发送命令后,接收命令的节点计算出该命令要处理的数据键所属的槽,当此槽是自己负责时直接执行命令,否则向客户端返回MOVED [正确的节点IP]错误(但这个信息不会被打印出来)并指引它重定向(再次发送命令)到正确的节点
    •  计算键所属的槽:算法
    • 判断当前节点是否负责槽:对比自己与clusterState.slots[i]的值
  • 重新分片:即重新进行槽指派,由集群管理软件redis-trib负责
  • ASK错误:重新分片可能引起的问题
  • 复制和故障转移:节点也分为master(用于处理槽)和slave(复制);设置slave命令CLUSTER REPLICATE nodeId;同理可故障检测、故障转移、选举master等
  • 节点通信:发送者sender发送消息message给接收者receiver;message包含消息头header和正文data,header包含了sender和receiver的信息;message分为:
    • MEET:请求加入集群
    • PING:节点默认以1秒每次的频率向节点列表中的5个随机节点中,最长时间没有发送过PING的节点发送PING消息,以检测该节点是否在线;随机性可能导致总是没有选到某个节点一致信息落后,于是节点的cluster-node-timeout选项,用于判断当节点最后一次收到PONG消息的时间到当前时间,如果超过了选项值的一半,则会向那个节点定向发送PING
    • PONG:回复
    • FAIL:节点判断另一个节点为fail时,广播FAIL消息,让其他节点标记此fail节点
    • PUBLISH:收到此命令的节点执行此命令,并广播此命令,让所有节点也执行此命令

16 发布与订阅

  • SUBSCRIBE:客户端订阅任意个频道channel,从而成为该频道的订阅者subscriber;其他客户端向该频道发送的所有消息,都将广播到该频道的所有subscriber;如subscribe "new"订阅了"new"频道
    • 频道的订阅关系存储在pubsub_channels字典中,key为某个频道,value为存储subscriber的链表;产生订阅关系时就在字典中关联,如果频道已经有其他subscriber,则当前subscriber追加到链表末尾,否则先建key,再建空链表并添加第一个元素
    • UNSUBSCRIBE反向命令即解除订阅关系;删除链表中的该元素;当链表为空时删除key
  • PSUBSCRIBE:客户端订阅任意个模式,从而成为该模式的订阅者,其他客户端向某个频道发送的消息不仅广播给该频道的所有subscriber,也发送给所有与该频道的模式相匹配的subscriber;如"new"和"naw"匹配"n[ea]w"模式,发送给"new"的消息也会被"naw"的接收到
    • 模式的订阅关系存储在pubsub_patterns链表中,元素为该模式可能的枚举值
    • PUNSUNSCRIBE为退订模式命令
  • 命令发布:PUBLISH channel message,消息发送给channel,然后获取subscriber链表(包括频道和模式的),遍历并发送message给它们
  • PUBSUB:
    • PUBSUB channel [pattern]:返回channel的信息;带有pattern参数则返回与pattern模式匹配的channel,否则返回所有channel;通过遍历key实现
    • PUBSUB NUMSUB [channel]:返回subscriber数量,即求链表长度
    • PUBSUB NUMPAT:返回订阅了该模式的subscriber数量,即求链表长度

17 Lua脚本

  • 在服务器中嵌入Lua环境,让客户端可以使用Lua脚本来原子的执行多个命令

18 其他

  • SORT排序:对列表键、集合键、有序集合键的值进行排序;可配合使用BY;ALPHA选项可对包含了字符串的值的键排序;默认ASC,可指定为DESC;配合LIMIT [offset,count]限制数量;STORE选项可保存排序结果;多个选项的执行顺序为:先排序(ALPHA,ASE,DESC,BY),再LIMIT,再GET,再STORE,最后返回结果集给客户端
  • 二进制位数组:
  • 慢查询日志:记录执行时间超过给定时长的命令的请求,以此功能日志来监视和优化查询速度
    • slowlog-log-slower-than:选项配置执行时间超过多少微妙时的命令会被记录到日志
    • slowlog-max-len:选项配置服务器最多保存多少慢查询条日志;日志FIFO即超过规格后删除最旧的一条
    • 可以使用CONFIG命令设置配置文件中的选项,如CONFIG SET slowlog-max-len 100
    • 相关属性:slowlog_entry_id记录新建一条慢查询日志时的日志id,从0开始,自增1;slowlog链表存储所有慢查询日志
    • 程序以微秒格式记录命令执行之前和之后的时间戳,差值即命令执行的时长,传给slowlogPushEntryIfNeeded函数来检查是否需要为此命令创建慢查询日志;slowlogCreateEntry函数创建慢查询日志,且让slowlog_entry_id加1
    • SLOWLOG GET查看慢查询日志;SLOWLOG LEN查看数量;SLOWLOG RESET清除慢查询日志
  • 监视器:执行MONITOR命令的客户端可成为监视器,实时接收和打印服务器处理的命令相关信息
  • 分布式锁:各个程序内部的锁,属于单机版的锁,属于JVM层面;不同于微服务架构,各个微服务之间为了避免冲突和数据故障,采用的分布式锁,即分布式系统中的java锁已经无力了,需要分布式锁
    • 实现分布式锁的解决方案:
      • MySQL:利用主键或唯一索引(作为锁的key)的唯一性

      • Zoopkeeper:利用顺序性临时节点

      • Redis:使用setnx key value和Redisson

        • 如SET userId:lock xxx NX 100:xxx需要保证全局唯一性,NX是原子操作性质的命令,表示当key不存在时,返回执行成功,执行成功表明服务成功获得锁,否则返回执行失败;100毫秒之后,key将自动删除;其他服务为了获得锁,使用value值不同的相同命令,然后如果key已存在且未过期,则返回执行失败即获取锁失败,服务进入循环等待状态,直到获得锁;如果前一个服务的业务执行时间超过100ms导致key超时(区别于key过期),后一个服务的命令将会执行成功,前一个服务需要对key续期(watch dog);业务完毕后为了释放锁,该服务会主动向Redis发起删除key的请求,在删除时需要判断value值是否一致,否则value是第二个服务创建的时便会误删;判断的逻辑需要lua脚本

        • 加锁后如果没有释放锁(delete key)或释放锁之前程序就挂了,则会出现死锁;因此需要手动释放锁,或利用key的过期机制

19 缓存

  • 为减轻数据库访问压力,将首次访问的数据建立缓存,后续访问时先查询缓存,有需要的数据则直接获取;否则再访问数据库

  • 缓存穿透:一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,就应该去后端系统查找(比如DB)。一些恶意的请求会故意查询不存在的key,请求量很大,就会对后端系统造成很大的压力,且是无效请求:避免:

    • 缓冲空对象:对底层数据查询结果为null的情况也进行缓存,缓存时间设置短一点,或者该key对应的数据insert了之后清理缓存

    • 布隆过滤器:一种数据结构,对所有可能的查询参数以hash形式存储,在控制层先进行校验,校验失败的丢弃

  • 缓存击穿:一个非常热点的key在不停扛着高并发,当缓存中该key过期时,高并发访问数据库,对数据库造成压力;避免:

    • 设置热点key永不过时

    • 添加分布式锁,以保证只有一个进程访问数据库

  • 缓存雪崩:当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,会给后端系统带来很大压力。导致系统崩溃;避免:

    • 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待

    • 做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期

    • 不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀

  • 缓存预热:提前把热数据放入Redis;开发上规避差集(即没有缓存的,这可用锁机制)以避免缓存各种问题

  • 缓存回收:后台轮询,分段分批回收过期的key

  • 缓存淘汰:在空间不足时,将某些缓存淘汰;淘汰策略有:

    • noeviction:当内存达到限制,且客户端尝试执行需要使用内存的命令时,返回错误

    • allKeys-lru:尝试回收最少使用的key

    • volatile-lru:尝试回收最少使用的key,但这些key仅限于已过期的集合中

    • allKeys-random:随机回收key

    • volatile-random:随机回收存在于过期集合中的key

    • volatile-ttl:回收过期集合中的key,且优先回收存活时间TTL较短的

    • allKeys-lfu:从所有key中回收使用频率最少的

    • volatile-lfu:从所有配置了过期时间的key中回收使用频率最少的

20 整合Java

20.1 Jedis

  • java操作Redis的连接开发工具包;直接连接,多线程不安全,最后使用连接池

  • 获取Jedis对象:Jedis jedis = new Jedis("127.0.0.1", "6379");该对象的方法对应了Redis的各种命令操作,如jedis.ping()测试连接;注意Redis服务器需要打开

  • 事务操作:

     Jedis jedis = new Jedis("127.0.0.1", "6379");
     JsonObject jsonObject = new JsonObject();
     jsonObject.put("userName", "name");
     jsonObject.put("age", 1);
     String result = jsonObject.toJSONString();
     Transaction transaction = jedis.multi();
     transaction.set("user1", result);
     transaction.set("user2", result);
     try {
         transaction.exec();
         jedis.get("user1");
     } catch () {
         transaction.discard();
     } finally {
         jedis.close();
     }

20.2 整合SpringBoot

  • 引入SpringData Redis;把Jedis换成了lettuce,实现了多线程环境的安全使用

  • 编写配置类,注册RedisTemplate到容器,包括自定义实现RedisTemplate的操作如自定义实现序列化

     @Configuration
     @EnableCaching
     public class RedisConfig extends CachingConfigurerSupport {
         // 缓存管理器
         @Bean
         public CacheManage cacheManager(RedisConnectionFactory redisConnectionFactory) {
             return RedisCacheManager.create(redisConnectionFactory);
         }
         
         public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
             RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
             redisTemplate.setConnectionFactory(redisConnectionFactory);
             
             Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Objcet.class);
             ObjectMapper objectMapper = new ObjectMapper();
             objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
             jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
             // 设置value的序列化采用Jackson2JsonRedisSerializer
             redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
             // 设置key的序列化采用StringRedisSerializer
             redisTemplate.setKeySerializer(new StringRedisSerializer());
             redisTemplate.afterPropertiesSet();
             return redisTemplate;
         }
     }

  • 配置连接:配置文件中配置host,port等属性

  • 连接测试及使用:同理,抽象了命令为方法,不过不太同名而已

    • 把对象最为value保存时,对象都应该被序列化;否则可能出现乱码等各种问题

    • 一般不使用原始方式,而是编写工具类RedisUtil(百度千篇一律)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值