石一歌的Redis笔记

Redis

Redis(Remote Dictionary Server ),即远程字典服务。

是一个开源的使用 ANSI C 语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API。

与 memcached 一样,为了保证效率,数据都是缓存在内存中。区别的是 redis 会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了 master-slave(主从) 同步。

  • 内存存储、持久化,内存是断电即失的,所以需要持久化(RDB、AOF)
  • 高效率、用于高速缓冲
  • 发布订阅系统
  • 地图信息分析
  • 计时器、计数器 (eg:浏览量)

安装

Windows

  • 下载地址

    官网不提供redis的windows版本;微软进行维护截止版本3.2.1;志愿者维护到5.0.14;

  • 使用

    • redis-server.exe 启动服务
    • redis-cli.exe 客户端

Linux

  • 下载地址

  • 解压

    tar -zvxf redis-6.2.6.tar.gz
    
  • 移动并改名为/usr/local/redis

    mv /root/redis-6.2.6 /usr/local/redis
    
  • 编译安装

    make
    make PREFIX=/usr/local/redis install #指定redis存放位置,方便删除
    
  • 启动

    ./bin/redis-server ./redis.conf #前台启动(如在配置文件设置daemonize属性为yes,变为后台启动)
    ./bin/redis-server& ./redis.conf #后台启动
    
  • redis.conf配置文件

    配置项名称 配置项值范围 说明
    daemonize yes、no yes表示启用守护进程,默认是no即不以守护进程方式运行。其中Windows系统下不支持启用守护进程方式运行
    port 指定 Redis 监听端口,默认端口为 6379
    bind 绑定的主机地址,如果需要设置远程访问则直接将这个属性备注下或者改为bind * 即可,这个属性和下面的protected-mode控制了是否可以远程访问 。
    protected-mode yes 、no 保护模式,该模式控制外部网是否可以连接redis服务,默认是yes,所以默认我们外网是无法访问的,如需外网连接rendis服务则需要将此属性改为no。
    timeout 300 当客户端闲置多长时间后关闭连接,如果指定为 0,表示关闭该功能
    loglevel debug、verbose、notice、warning 日志级别,默认为 notice
    databases 16 设置数据库的数量,默认的数据库是0。整个通过客户端工具可以看得到
    rdbcompression yes、no 指定存储至本地数据库时是否压缩数据,默认为 yes,Redis 采用 LZF 压缩,如果为了节省 CPU 时间,可以关闭该选项,但会导致数据库文件变的巨大。
    dbfilename dump.rdb 指定本地数据库文件名,默认值为 dump.rdb
    dir 指定本地数据库存放目录
    requirepass 设置 Redis 连接密码,如果配置了连接密码,客户端在连接 Redis 时需要通过 AUTH 命令提供密码,默认关闭
    maxclients 0 设置同一时间最大客户端连接数,默认无限制,Redis 可以同时打开的客户端连接数为 Redis 进程可以打开的最大文件描述符数,如果设置 maxclients 0,表示不作限制。当客户端连接数到达限制时,Redis 会关闭新的连接并向客户端返回 max number of clients reached 错误信息。
    maxmemory XXX 指定 Redis 最大内存限制,Redis 在启动时会把数据加载到内存中,达到最大内存后,Redis 会先尝试清除已到期或即将到期的 Key,当此方法处理 后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis 新的 vm 机制,会把 Key 存放内存,Value 会存放在 swap 区。配置项值范围列里XXX为数值。
  • 查看redis

    ps -aux | grep redis #进程名查看
    netstat -lanp | grep 6379 #端口号查看
    
  • redis-cli登录客户端

    ./bin/redis-cli -h 127.0.0.1 -p 6379 #默认本地 默认6379
    shutdown #关闭redis服务
    exit #退出客户端
    

压力测试

redis-benchmark:Redis 官方提供的性能测试工具

QQ截图20220112191020

redis-benchmark -h localhost -p 6379 -c 100 -n 10000

QQ截图20220112200609

基础知识

  • 数据库

    • redis默认有16个数据库,默认使用0号数据库。

    • 常用命令

      config get databases #命令行查看数据库数量.
      select n #切换到数据库 n。
      dbsize #可以查看当前数据库的大小,与 key 数量相关。
      keys * #查看当前数据库中所有的 key。
      flushdb #清空当前数据库中的键值对。
      flushall #清空所有数据库的键值对。
      
  • 性能

    • 快的原因

      • 绝大部分请求是纯粹的内存操作(非常快速)

      • 采用单线程,避免了不必要的上下文切换和竞争条件

      • 非阻塞IO - IO多路复用

    • 单线程

      Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。 Redis 的瓶颈并不在 CPU,而在内存和网络。如果要使用 CPU 多核,可以搭建多个 Redis 实例来解决。

    • 对比多线程

      多线程缺点:它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,增加了系统复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗。

      单线程优点:使 Redis 内部实现的复杂度大大降低,Hash 的惰性 Rehash、Lpush 等等 “线程不安全” 的命令都可以无锁进行。Redis 通过 AE 事件模型以及 IO 多路复用等技术,处理性能非常高。

    • 6.0引入的多线程

      充分利用服务器 CPU 资源,目前单线程只能利用一个核。

      多线程部分只是用来处理网络数据的读写和协议解析,以此提高性能。

五大数据类型

Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings)散列(hashes)列表(lists)集合(sets)有序集合(sorted sets) 与范围查询, bitmapshyperloglogs地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication)LUA脚本(Lua scripting)LRU驱动事件(LRU eviction)事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。

Redis-Key

在 redis 中无论什么数据类型,在数据库中都是以 key-value 形式保存,通过进行对 Redis-key 的操作,来完成对数据库中数据的操作。

  • 常见命令

    exists key #判断键是否存在
    del key #删除键值对
    move key db #将键值对移动到指定数据库
    expire key second #设置键值对的过期时间
    #当前 key 没有设置过期时间,所以会返回 - 1.
    #当前 key 有设置过期时间,而且 key 已经过期,所以会返回 - 2.
    #当前 key 有设置过期时间,且 key 还没有过期,故会返回 key 的正常剩余时间.
    ttl key #查看过期时间
    type key #查看 value 的数据类型
    RENAME key newkey #修改 key 的名称
    RENAMENX key newkey #仅当 newkey 不存在时,将 key 改名为 newkey 。
    
  • 官网

String

底层:Redis自己构建了SDS的抽象数据类型,作为 Redis 的默认字符串表示。具体的底层实现为int(8个字节的长整型)+embstr(小于等于39个字节的字符串)+raw(大于39个字节的字符串)。

redis3.2版本后将分界线改为44字节

  • 命令

    命令 描述 示例
    APPEND key value 向指定的 key 的 value 后追加字符串 set msg hello
    OK
    append msg "world"
    (integer) 11
    get msg
    “hello world”
    DECR/INCR key 将指定 key 的 value 数值进行 + 1/-1(仅对于数字) set age 20
    OK
    incr age
    (integer) 21
    decr age
    (integer) 20
    INCRBY/DECRBY key n 按指定的步长对数值进行加减 INCRBY age 5
    (integer) 25
    DECRBY age 10
    (integer) 15
    INCRBYFLOAT key n 为数值加上浮点型数值 INCRBYFLOAT age 5.2
    “20.2”
    STRLEN key 获取 key 保存值的字符串长度 get msg
    “hello world”
    STRLEN msg
    (integer) 11
    GETRANGE key start end 按起止位置获取字符串(闭区间,起止位置都取) get msg
    “hello world”
    GETRANGE msg 3 9
    “lo worl”
    SETRANGE key offset value 用指定的 value 替换 key 中 offset 开始的值 SETRANGE msg 2 hello
    (integer) 7
    get msg
    “tehello”
    GETSET key value 将给定 key 的值设为 value ,并返回 key 的旧值 (old value)。如果不存在,则返回 nil GETSET msg test
    “hello world”
    SETNX key value 仅当 key 不存在时进行 set
    分布式锁
    SETNX msg test
    (integer) 0
    SETNX name sakura
    (integer) 1
    SETEX key seconds value set 键值对并设置过期时间 setex name 10
    OK
    get name
    (nil)
    MSET key1 value1 [key2 value2..] 批量 set 键值对 MSET k1 v1 k2 v2 k3 v3
    OK
    MSETNX key1 value1 [key2 value2..] 批量设置键值对,仅当参数中所有的 key 都不存在时执行
    原子性操作
    MSETNX k1 v1 k4 v4
    (integer) 0
    MGET key1 [key2..] 批量获取多个 key 保存的值 MGET k1 k2 k3
    1) “v1”
    2) “v2”
    3) “v3”
    PSETEX key milliseconds value 和 SETEX 命令相似,但它以毫秒为单位设置 key 的生存时间, getset db redis
    (nil)
    get db
    "redis"
    getset db lisa
    "redis"
    get db
    "lisa"
  • 应用

    单值缓存
    SET  key  value 	
    GET  key 	
     
    对象缓存
    1) SET  user:1  value(json格式数据)
    2) MSET  user:1:name  zhansan   user:1:balance  1888
        MGET  user:1:name   user:1:balance 
     
    分布式锁
    SETNX  product:10001  true 		//返回1代表获取锁成功
    SETNX  product:10001  true 		//返回0代表获取锁失败
    。。。执行业务操作
    DEL  product:10001			//执行完业务释放锁
     
    SET product:10001 true  ex  10  nx	//防止程序意外终止导致死锁
     
    计数器
    INCR article:readcount:{文章id}  	
    GET article:readcount:{文章id} 
     
    Web集群session共享
    spring session + redis实现session共享
     
    分布式系统全局序列号	
    INCRBY  orderId  1000		//redis批量生成序列号提升性能
    
    限速
    处于安全考虑,每次进行登录时让用户输入手机验证码,为了短信接口不被频繁访问,会限制用户每分钟获取验证码的频率。
    

List

Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)

一个列表最多可以包含 2的32次 - 1 个元素 (4294967295, 每个列表超过 40 亿个元素)。

底层:3.2版本之前是ziplist(列表对象所有字符串元素长度都小于64个字节&&元素数量小于512)+linkedlist,3.2版本之后都为quicklist

  • 命令

    命令 描述 示例
    LPUSH/RPUSH key value1[value2..] 从左边 / 右边向列表中 PUSH 值 (一个或者多个)
    普通的get是无法获取list值的
    LPUSH mylist k1
    (integer) 1
    LPUSH mylist k2
    (integer) 2
    RPUSH mylist k3
    (integer) 3
    get mylist
    (error) WRONGTYPE Operation against a key holding the wrong kind of value
    LRANGE key start end 获取 list 起止元素 (索引从左往右 递增) LRANGE mylist 0 2
    1) "k2"
    2) "k1"
    3) "k3"
    LRANGE mylist 0 -1
    1) "k2"
    2) "k1"
    3) "k3"
    LPUSHX/RPUSHX key value 向已存在的列名中 push 值(一个或者多个)
    list不存在 LPUSHX失败
    LPUSHX list v1
    (integer) 0
    LPUSHX mylist k4 k5
    (integer) 5
    LRANGE mylist 0 -1
    1) "k5"
    2) "k4"
    3) "k2"
    4) "k1"
    5) "k3"
    `LINSERT key BEFORE AFTER pivot value` 在指定列表元素的前 / 后 插入 value
    LLEN key 查看列表长度 LLEN mylist
    (integer) 6
    LINDEX key index 通过索引获取列表元素 LINDEX mylist 3
    "ins_key1"
    LSET key index value 通过索引为元素设值 LSET mylist 3 k6
    OK
    LRANGE mylist 0 -1
    1) "k5"
    2) "k4"
    3) "k2"
    4) "k6"
    5) "k1"
    6) "k3"
    LPOP/RPOP key 从最左边 / 最右边移除值 并返回 LPOP mylist
    "k5"
    RPOP mylist
    "k3"
    RPOPLPUSH source destination 将列表的尾部 (右) 最后一个值弹出,并返回,然后加到另一个列表的头部 LRANGE mylist 0 -1
    1) "k4"
    2) "k2"
    3) "k6"
    4) "k1"
    RPOPLPUSH mylist newlist
    "k1"
    LRANGE newlist 0 -1
    1) "k1"
    LRANGE mylist 0 -1
    1) "k4"
    2) "k2"
    3) "k6"
    LTRIM key start end 通过下标截取指定范围内的列表 LTRIM mylist 0 1
    OK
    LRANGE mylist 0 -1
    1) "k4"
    2) "k2"
    LREM key count value List 中是允许 value 重复的 count > 0:从头部开始搜索 然后删除指定的 value 至多删除 count 个 count < 0:从尾部开始搜索… count = 0:删除列表中所有的指定 value。 flushdb
    RPUSH mylist k2 k2 k2 k2 k2 k2 k4 k2 k2 k2 k2
    LREM mylist 3 k2
    (integer) 3
    LREM mylist -2 k2
    (integer) 2
    lrange mylist 0 5
    1) "k2"
    2) "k2"
    3) "k2"
    4) "k4"
    5) "k2"
    6) "k2"
    BLPOP/BRPOP key1[key2] timout 移出并获取列表的第一个 / 最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 lpush newlist k1
    (integer) 1
    lrange newlist 0 -1
    1) "k1"
    blpop newlist 3
    1) "newlist"
    2) "k1"
    blpop newlist 3
    (nil)
    (3.02s)
    BRPOPLPUSH source destination timeout RPOPLPUSH功能相同,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 lpush newlist k1
    (integer) 1
    brpoplpush newlist mylist 3
    "k1"
    brpoplpush newlist mylist 3
    (nil)
    (3.02s)
  • list 实际上是一个链表,before Node after , left, right 都可以插入值

  • 如果 key 不存在,则创建新的链表

  • 如果 key 存在,新增内容

  • 如果移除了所有值,空链表,也代表不存在

  • 在两边插入或者改动值,效率最高!修改中间元素,效率相对较低

  • 应用

    栈:LPUSH +LPOP -->FILO
    先进后出原则:LPUSH从队列左边进入d,c,b,a, LPOP从队列左边出来a,b,c,d
    
    队列: LPUSH+RPOP
    先进先出原则:LPUSH从队列左边进入d,c,b,a, RPOP从队列右边出d,c,b,a
    
    阻塞队列(消息队列): LPUSH+BRPOP
    LPUSH+BRPOP是在LPUSH+RPOP的基础上多了阻塞和等待的功能,
    BRPOP实际上就是等于Blocking+RPOP,当队列中的数据为空时,
    会一直监听消息队列,直到获得消息
    
    排行榜,数据最新列表
    

Set

Redis 的 Set 是 string 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。

Redis 中 集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。

集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储 40 多亿个成员)。

底层: intset(集合对象保存的所有元素都是整数值&&集合对象保存的元素数量不超过 512 个)+hashtable两种实现方式

  • 命令

    命令 描述 示例
    SADD key member1[member2..] 向集合中无序增加一个 / 多个成员 sadd myset shiyige
    (integer) 1
    sadd myset study
    (integer) 1
    sadd myset everyday
    (integer) 1
    SCARD key 获取集合的成员数 scard myset
    (integer) 3
    SMEMBERS key 返回集合中所有的成员 smembers myset
    1) "everyday"
    2) "shiyige"
    3) "study"
    SISMEMBER key member 查询 member 元素是否是集合的成员, 结果是无序的 sismember myset shiyige
    (integer) 1
    sismember myset kuang
    (integer) 0
    SRANDMEMBER key [count] 随机返回集合中 count 个成员,count 缺省值为 1 srandmember myset
    "everyday"
    SPOP key [count] 随机移除并返回集合中 count 个成员,count 缺省值为 1 spop myset
    "study"
    SMOVE source destination member 将 source 集合的成员 member 移动到 destination 集合 smembers myset
    1) "everyday"
    2) "shiyige"
    smove myset newset everyday
    (integer) 1
    smembers myset
    1) "shiyige"
    smembers newset
    1) "everyday"
    SREM key member1[member2..] 移除集合中一个 / 多个成员 srem myset shiyige
    (integer) 1
    srem newset everyday
    (integer) 1
    SDIFF key1[key2..] 返回所有集合的差集 key1- key2 - … sadd setA a b c
    (integer) 3
    sadd setB c d e
    (integer) 3
    sdiff setA setB
    1) "a"
    2) "b"
    SDIFFSTORE destination key1[key2..] 在 SDIFF 的基础上,将结果保存到集合中 (覆盖)。不能保存到其他类型 key 噢! sdiffstore diffSet setA setB
    (integer) 2
    smembers diffSet
    1) "a"
    2) "b"
    SINTER key1 [key2..] 返回所有集合的交集 sinter setA setB
    1) "c"
    SINTERSTORE destination key1[key2..] 在 SINTER 的基础上,存储结果到集合中。覆盖 sinterstore interSet setA setB
    (integer) 1
    smembers interSet
    1) "c"
    SUNION key1 [key2..] 返回所有集合的并集 sunion setA setB
    1) "c"
    2) "a"
    3) "b"
    4) "d"
    5) "e"
    SUNIONSTORE destination key1 [key2..] 在 SUNION 的基础上,存储结果到到集合中。覆盖 sunionstore unionSet setA setB
    (integer) 5
    smembers unionSet
    1) "c"
    2) "a"
    3) "b"
    4) "d"
    5) "e"
    SSCAN KEY [MATCH pattern] [COUNT count] 在大量数据环境下,使用此命令遍历集合中元素,每次遍历部分 sscan unionSet 0
    1) "0"
    2) 1) "d"
    2) "a"
    3) "c"
    4) "b"
    5) "e"
  • 应用

    微信抽奖小程序
    SRANDMENBER act:1008 1 随机抽取一个并在集合中删除
    
    共同关注:求交集
    SINTER key1 [key2..]
    
    可能认识的人:求差集
    SDIFF key1[key2..]
    
    点赞、收藏、标签
        1)点赞的人:SADD like:1 1001 1002 1003 1004 1005
        2)取消点赞:SREM like:1 1002
        3)检查用户是否点赞过:
            SISMEMBER like:1 1002
            SISMEMBER like:1 1005
        4)获取点赞人员列表:SMEMBERS like:1
        5)获取点赞总人数:SCARD like:1
    

Hash

Redis hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。

Set 就是一种简化的 Hash, 只变动 key, 而 value 使用默认值填充。可以将一个 Hash 表作为一个对象进行存储,表中存放对象的信息。

底层:ziplist(哈希对象保存的所有键值对的键和值的字符串长度都小于 64 字节&&哈希对象保存的键值对数量小于 512 个)+hashtable两种实现方式

  • 命令

    命令 描述 示例
    HSET key field value 将哈希表 key 中的字段 field 的值设为 value 。重复设置同一个 field 会覆盖, 返回 0 hset myhash name shiyige
    (integer) 1
    HMSET key field1 value1 [field2 value2..] 同时将多个 field-value (域 - 值) 对设置到哈希表 key 中。
    redis4.0弃用
    hset myhash age 20 grade a
    (integer) 2
    HSETNX key field value 只有在字段 field 不存在时,设置哈希表字段的值。 hsetnx myhash phone 13027083807
    (integer) 1
    HEXISTS key field 查看哈希表 key 中,指定的字段是否存在。 hexists myhash name
    (integer) 1
    HGET key field value 获取存储在哈希表中指定字段的值 hget myhash name
    "shiyige"
    HMGET key field1 [field2..] 获取所有给定字段的值
    redis4.0弃用
    hmget myhash name age grade phone
    1) "shiyige"
    2) "20"
    3) "a"
    4) "13027083807"
    HGETALL key 获取在哈希表 key 的所有字段和值 hgetall myhash
    1) "name"
    2) "shiyige"
    3) "age"
    4) "20"
    5) "grade"
    6) "a"
    7) "phone"
    8) "13027083807"
    HKEYS key 获取哈希表 key 中所有的字段 hkeys myhash
    1) "name"
    2) "age"
    3) "grade"
    4) "phone"
    HLEN key 获取哈希表中字段的数量 hlen myhash
    (integer) 4
    HVALS key 获取哈希表中所有值 hvals myhash
    1) "shiyige"
    2) "20"
    3) "a"
    4) "13027083807"
    HDEL key field1 [field2..] 删除哈希表 key 中一个 / 多个 field 字段 hdel myhash phone
    (integer) 1
    HINCRBY key field n 为哈希表 key 中的指定字段的整数值加上增量 n,并返回增量后结果 一样只适用于整数型字段 hincrby myhash age 1
    (integer) 21
    HINCRBYFLOAT key field n 为哈希表 key 中的指定字段的浮点数值加上增量 n。 hset myhash temperature 36.5
    (integer) 1
    hincrbyfloat myhash temperature 1
    "37.5"
    HSCAN key cursor [MATCH pattern] [COUNT count] 迭代哈希表中的键值对。 hscan myhash 0
    1) "0"
    2) 1) "name"
    2) "shiyige"
    3) "age"
    4) "21"
    5) "grade"
    6) "a"
    7) "temperature"
    8) "37.5"
  • 应用

    对象缓存
    当对象的某个属性需要频繁修改时,不适合用string+json,一般使用hash
    
    购物车
    hset cart:10001 10010 1	
    hdel cart:10001 10010 
    

Zset

不同的是每个元素都会关联一个 double 类型的分数(score)。redis 正是通过分数来为集合中的成员进行从小到大的排序。

score 相同:按字典顺序排序

有序集合的成员是唯一的, 但分数 (score) 却可以重复。

底层:ziplist(有序集合保存的元素数量小于 128 个&&有序集合保存的所有元素成员的长度都小于 64 字节)+skiplist两种实现方式

  • 命令(启动客户端加入--raw,支持汉字)

    命令 描述 示例
    ZADD key score member1 [score2 member2] 向有序集合添加一个或多个成员,或者更新已存在成员的分数 zadd salary 5000 石一歌 8000 诗意歌 10000 拾遗阁
    (integer) 3
    ZCARD key 获取有序集合的成员数 zcard salary
    (integer) 3
    ZCOUNT key min max 计算在有序集合中指定区间 score 的成员数 zcount salary 8000 12000
    (integer) 2
    ZINCRBY key n member 有序集合中对指定成员的分数加上增量 n zincrby salary 2000 石一歌
    "7000"
    ZSCORE key member 返回有序集中,成员的分数值 zscore salary 石一歌
    "7000"
    ZRANK key member 返回有序集合中指定成员的索引 zrank salary 拾遗阁
    (integer) 2
    ZRANGE key start end 通过索引区间返回有序集合成指定区间内的成员 zrange salary 0 -1
    石一歌
    诗意歌
    拾遗阁
    zrange salary 0 -1 withscores
    石一歌
    5000
    诗意歌
    8000
    拾遗阁
    10000
    ZRANGEBYLEX key min max 通过字典区间返回有序集合的成员 zrangebylex salary - +
    石一歌
    诗意歌
    拾遗阁
    ZRANGEBYSCORE key min max 通过分数返回有序集合指定区间内的成员 -inf 和 +inf 分别表示最小最大值,只支持开区间 () zrangebyscore salary -inf +inf withscores
    石一歌
    5000
    诗意歌
    8000
    拾遗阁
    10000
    ZLEXCOUNT key min max 在有序集合中计算指定字典区间内成员数量 zlexcount salary - +
    3
    ZREM key member1 [member2..] 移除有序集合中一个 / 多个成员 zrem salary 拾遗阁 石一歌 诗意歌
    3
    ZREMRANGEBYLEX key min max 移除有序集合中给定的字典区间的所有成员 zadd myZset 7 ujm 66 yhn 555 tgb 4 rfv 33 edc 222 wsx 1111 qaz
    7
    zremrangebylex myZset [edc [tab
    1
    ZREMRANGEBYRANK key start stop 移除有序集合中给定的排名区间的所有成员 zremrangebyrank myZset 0 1
    2
    ZREMRANGEBYSCORE key min max 移除有序集合中给定的分数区间的所有成员 zremrangebyscore myZset 50 1200
    4
    ZREVRANGE key start end 返回有序集中指定区间内的成员,通过索引,分数从高到底 zrevrange myZset 0 3
    qaz
    tgb
    wsx
    yhn
    ZREVRANGEBYSCORRE key max min 返回有序集中指定分数区间内的成员,分数从高到低排序 zrevrangebyscore myZset 500 0
    wsx
    yhn
    edc
    ujm
    rfv
    ZREVRANGEBYLEX key max min 返回有序集中指定字典区间内的成员,按字典顺序倒序 zrevrangebylex myZset [wsx [edc
    qaz
    tgb
    wsx
    yhn
    ZREVRANK key member 返回有序集合中指定成员的排名,有序集成员按分数值递减 (从大到小) 排序 zrevrank myZset wsx
    2
    ZINTERSTORE destination numkeys key1 [key2 ..] 计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 key 中,numkeys:表示参与运算的集合数,将 score 相加作为结果的 score zinterstore sumscore 2 mathscore enscore
    3
    ZUNIONSTORE destination numkeys key1 [key2..] 计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 key 中 zunionstore lowestscore 2 mathscore enscore
    3
    zadd mathscore 99 xm 90 xh 89 xg
    3
    zadd enscore 75 xm 90 xh 96 xg
    3
    zinterstore sumscore 2 mathscore enscore
    3
    ZSCAN key cursor [MATCH pattern\] [COUNT count] 迭代有序集合中的元素(包括元素成员和元素分值) zscan lowestscore 0
    0
    xm
    174
    xh
    180
    xg
    185
  • 应用

    延时队列
    zset 会按 score 进行排序,如果 score 代表想要执行时间的时间戳。在某个时间将它插入 zset 集合中,它变会按照时间戳大小进行排序,也就是对执行时间前后进行排序。
    起一个死循环线程不断地进行取第一个 key 值,如果当前时间戳大于等于该 key 值的 score 就将它取出来进行消费删除,可以达到延时执行的目的。
    
    排行榜
    以当前小时的时间戳作为 zset 的 key,把贴子ID 作为 member ,点击数评论数等作为 score,当 score 发生变化时更新 score。利用 ZREVRANGE 或者 ZRANGE 查到对应数量的记录
    
    限流
    滑动窗口是限流常见的一种策略。如果我们把一个用户的 ID 作为 key 来定义一个 zset ,member 或者 score 可以都为访问时的时间戳。我们只需统计某个 key 下在指定时间戳区间内的个数,就能得到这个用户滑动窗口内访问频次,与最大通过次数比较,来决定是否允许通过。
    

应用场景总结

QQ截图20220114002402

三大特殊数据类型

Geospatial

使用经纬度定位地理坐标并用一个有序集合 zset 保存,可使用 zset 命令

经度 [-180,180] ,纬度 [- 85.05112878,85.05112878]

底层:zset

  • 命令

    命令 描述 示例
    geoadd key longitud(经度) latitude(纬度) member [..] 将具体经纬度的坐标存入一个有序集合 geoadd china:city 112.53 37.86 太原 116.24 39.55 北京 121.29 31.14 上海 117.12 39.02 天津 106.33 29.35 重庆 114.06 22.61 深圳 113.34 22.17 珠海 116.41 23.22 汕头 118.10 24.46 厦门 110.20 20.02 海南 75.59 39.30 喀什 88.39 44.37 霍尔果斯
    12
    geopos key member [member..] 获取集合中的一个 / 多个成员坐标 geopos china:city 太原
    112.53000050783157349
    37.86000073876942196
    geodist key member1 member2 [unit] 返回两个给定位置之间的距离。默认以米作为单位。 geodist china:city 太原 北京 km
    372.8209
    `georadius key longitude latitude radius m km mi
    GEORADIUSBYMEMBER key member radius... 功能与 GEORADIUS 相同,只是中心位置不是具体的经纬度,而是使用结合中已有的成员作为中心点。 georadiusbymember china:city 太原 1000 km withcoord withdist count 5 asc
    太原
    0.0000
    112.53000050783157349
    37.86000073876942196
    北京
    372.8209
    116.23999983072280884
    39.5500007245470826
    天津
    420.1181
    117.12000042200088501
    39.02000066901394604
    geohash key member1 [member2..] 返回一个或多个位置元素的 Geohash 表示。使用 Geohash 位置 52 点整数编码。 geohash china:city 太原
    ww8p2sjy7p0
  • 应用

    朋友定位
    geopos china:member xx
    
    附近的人
    georadius china:city 113.34 37.51 500 m withdist count 20 asc
    
    打车距离计算
    geodist china:city 太原站 中北 km
    

Hyperloglog

Redis HyperLogLog 是用来做基数(数据集中不重复的元素的个数。)统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。

花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。

因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。

底层: string

  • 命令

    命令 描述 示例
    PFADD key element1 [elememt2..] 添加指定元素到 HyperLogLog 中 pfadd myHlla a s d c f v g b h n
    (integer) 1
    PFCOUNT key [key] 返回给定 HyperLogLog 的基数估算值。 pfcount myHlla
    (integer) 10
    PFMERGE destkey sourcekey [sourcekey..] 将多个 HyperLogLog 合并为一个 HyperLogLog pfadd myHllb q a z w s x e d c
    (integer) 1
    pfcount myHllb
    (integer) 9
    pfmerge newHll myHlla myHllb
    OK
    pfcount newHll
    (integer) 15
  • 应用

    统计注册 IP 数
    统计每日访问 IP 数
    统计页面实时 UV 数
    统计在线用户数
    统计用户每天搜索不同词条的个数
    

Bitmaps

使用位存储,信息状态只有 0 和 1

Bitmap 是一串连续的 2 进制数字(0 或 1),每一位所在的位置为偏移 (offset),在 bitmap 上可执行 AND,OR,XOR,NOT 以及其它位操作。

底层:byte数组

  • 命令

    命令 描述 示例
    setbit key offset value 为指定 key 的 offset 位设置值 setbit sign 0 1
    (integer) 0
    setbit sign 1 1
    (integer) 0
    setbit sign 2 1
    (integer) 0
    setbit sign 3 0
    (integer) 0
    setbit sign 4 0
    (integer) 0
    getbit key offset 获取 offset 位的值 getbit sign 1
    (integer) 1
    bitcount key [start end] 统计字符串被设置为 1 的 bit 数,也可以指定统计范围按字节 bitcount sign
    (integer) 3
    bitop operration destkey key[key..] 对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上。 setbit signtwo 0 1
    (integer) 0
    setbit signtwo 1 0
    (integer) 1
    setbit signtwo 2 1
    (integer) 0
    setbit signtwo 3 0
    (integer) 0
    setbit signtwo 4 1
    (integer)
    0bitop and newsign sign signtwo
    (integer) 1
    bitcount newsign
    (integer) 2
    BITPOS key bit [start] [end] 返回字符串里面第一个被设置为 1 或者 0 的 bit 位。start 和 end 只能按字节, 不能按位 bitpos sign 1
    (integer) 0
    bitpos sign 0
    (integer) 3
  • 应用

    签到统计、状态统计
    

事务

Redis 的单条命令是保证原子性的,但是 redis 事务不能保证原子性

一次性、顺序性、排他性

事务过程

  • 开启事务(multi
  • 命令入队
  • 执行事务(exec
  • 取消事务 (discurd)

事务错误

  • 代码语法错误 (编译时异常)所有的命令都不执行
  • 代码逻辑错误 (运行时异常)其他命令可以正常执行 >> 事务无原子性

监控(乐观锁)

获取 version

更新的时候比较 version

  • 使用watch key监控指定数据,相当于乐观锁加锁。
  • 进行事务,每次提交执行 exec 后都会自动释放锁,不管是否成功
  • 使用unwatch解除监控

jedis

  • pom

     	    <!--jedis-->
            <dependency>
                <groupId>redis.clients</groupId>
                <artifactId>jedis</artifactId>
                <version>4.0.0</version>
            </dependency>
    
  • 示例

    以上所讲的各种操作,均为jedis实例化后的方法。

        public static void main(String[] args) {
            Jedis jedis = new Jedis("192.168.xx.xxx", 6379);
            String response = jedis.ping();
            System.out.println(response);
            jedis.close();
        }
    

springboot整合

jedis : 采用的直连,多个线程操作的话,是不安全的,如果想要避免不安全的,使用 jedis pool 连接池! 更像 BIO 模式

lettuce : 采用netty,实例可以再多个线程中进行共享,不存在线程不安全的情况!可以减少线程数据,更像 NIO 模式

  • pom(Lettuce的依赖可能会有问题,将阿里的镜像换为指定的仓库后解决)

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
    

源码

  • RedisProperties

    @ConfigurationProperties(prefix = "spring.redis")
    public class RedisProperties {
    
    	/**
    	 * Database index used by the connection factory.
    	 */
    	private int database = 0;
    
    	/**
    	 * Connection URL. Overrides host, port, and password. User is ignored. Example:
    	 * redis://user:password@example.com:6379
    	 */
    	private String url;
    
    	/**
    	 * Redis server host.
    	 */
    	private String host = "localhost";
    
    	/**
    	 * Login username of the redis server.
    	 */
    	private String username;
    
    	/**
    	 * Login password of the redis server.
    	 */
    	private String password;
    	...
        private final Jedis jedis = new Jedis();
    
    	private final Lettuce lettuce = new Lettuce();
        ...
    }
    
    • 没什么好说的,简单的配置类,绑定了spring.redis,其中的私有变量也说明底层是JedisLettuce
  • RedisAutoConfiguration

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(RedisOperations.class)
    @EnableConfigurationProperties(RedisProperties.class)
    @Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
    public class RedisAutoConfiguration {
    
    	@Bean
    	@ConditionalOnMissingBean(name = "redisTemplate")
    	@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    	public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
    		RedisTemplate<Object, Object> template = new RedisTemplate<>();
    		template.setConnectionFactory(redisConnectionFactory);
    		return template;
    	}
    
    	@Bean
    	@ConditionalOnMissingBean
    	@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    	public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
    		return new StringRedisTemplate(redisConnectionFactory);
    	}
    
    }
    
    • @Configuration(proxyBeanMethods = false),说明该类是配置类,其中的参数是开启Lite模式,返回新实例对象,以加快启动速度。

    • @ConditionalOnClass(RedisOperations.class),构建条件为存在RedisOperations类。

    • @EnableConfigurationProperties(RedisProperties.class),将RedisProperties@ConfigurationProperties标注的类)进行注入,让配置文件生效。

    • @Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class }),通过@Import注解方式生成类实例并注入Spring容器。

      • LettuceConnectionConfiguration

        @Configuration(proxyBeanMethods = false)
        @ConditionalOnClass(RedisClient.class)
        @ConditionalOnProperty(name = "spring.redis.client-type", havingValue = "lettuce", matchIfMissing = true)
        class LettuceConnectionConfiguration extends RedisConnectionConfiguration {
        
        	LettuceConnectionConfiguration(RedisProperties properties,
        			ObjectProvider<RedisStandaloneConfiguration> standaloneConfigurationProvider,
        			ObjectProvider<RedisSentinelConfiguration> sentinelConfigurationProvider,
        			ObjectProvider<RedisClusterConfiguration> clusterConfigurationProvider) {
        		super(properties, standaloneConfigurationProvider, sentinelConfigurationProvider, clusterConfigurationProvider);
        	}
        
        	...
        
        	@Bean
        	@ConditionalOnMissingBean(RedisConnectionFactory.class)
        	LettuceConnectionFactory redisConnectionFactory(
        			ObjectProvider<LettuceClientConfigurationBuilderCustomizer> builderCustomizers,
        			ClientResources clientResources) {
        		LettuceClientConfiguration clientConfig = getLettuceClientConfiguration(builderCustomizers, clientResources,
        				getProperties().getLettuce().getPool());
        		return createLettuceConnectionFactory(clientConfig);
        	}
        
        	private LettuceConnectionFactory createLettuceConnectionFactory(LettuceClientConfiguration clientConfiguration) {
        		if (getSentinelConfig() != null) {
        			return new LettuceConnectionFactory(getSentinelConfig(), clientConfiguration);
        		}
        		if (getClusterConfiguration() != null) {
        			return new LettuceConnectionFactory(getClusterConfiguration(), clientConfiguration);
        		}
        		return new LettuceConnectionFactory(getStandaloneConfig(), clientConfiguration);
        	}
        
        	private LettuceClientConfiguration getLettuceClientConfiguration(
        			ObjectProvider<LettuceClientConfigurationBuilderCustomizer> builderCustomizers,
        			ClientResources clientResources, Pool pool) {
        		LettuceClientConfigurationBuilder builder = createBuilder(pool);
        		applyProperties(builder);
        		if (StringUtils.hasText(getProperties().getUrl())) {
        			customizeConfigurationFromUrl(builder);
        		}
        		builder.clientOptions(createClientOptions());
        		builder.clientResources(clientResources);
        		builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
        		return builder.build();
        	}
        
        	...
        
        }
        
      • @Configuration(proxyBeanMethods = false),说明该类是配置类,其中的参数是开启Lite模式,返回新实例对象,以加快启动速度。

      • @ConditionalOnClass(RedisClient.class),构建条件为存在RedisClient类。

      • @ConditionalOnProperty(name = "spring.redis.client-type", havingValue = "lettuce", matchIfMissing = true),构建条件为配置spring.redis.client-type=lettuce。

      • LettuceConnectionConfiguration方法,上一文件的@Import注解,通过构造方法将对应的实例注入到spring容器,Ledis和Lettuce的构造方法都有RedisProperties,因此我们的通过配置文件写的个人配置和默认配置都被底层实现获取。具体到LettuceConnectionConfiguration,使用继承自RedisConnectionConfiguration的super方法获取配置。

      • redisConnectionFactory方法,在没有RedisConnectionFactory这个bean的情况下,会扫描到redisConnectionFactory()方法并返回实体,并注入到Spring容器。实际上返回的是LettuceConnectionFactory

      • createLettuceConnectionFactory方法,会根据配置的redis参数判断用单机/哨兵/集群模式来创建LettuceConnectionFactory实例。

      • getLettuceClientConfiguration方法,接受客户端参数,创建客户端配置对象。

      以上创建并注入了LettuceConnectionFactory实例,包含客户端连接池。

      回到上一级

    • RedisTemplate方法,在没有RedisTemplate这个bean且RedisConnectionFactory在容器中只有一个的情况下,会扫描到redisTemplate()方法并返回实体,并注入到Spring容器。该方法有一个RedisConnectionFactory参数。而上一步中redisConnectionFactory方法最后会注入一个LettuceConnectionFactory实例,而LettuceConnectionFactory又继承于RedisConnectionFactory,故我们的配置就能通过LettuceConnectionFactoryRedisTemplate拿到,以此来进行redis的操控。

  • 思考一下,开始时通过@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })注入了Lettuce和Jedis两个连接配置实例,并且注入时都是对应RedisConnectionFactory类的。那么redisTemplate方法最后是使用哪个实例来创建RedisTemplate的呢?

    其实由于@ConditionalOnMissingBean(RedisConnectionFactory.class)的约束,排在后面的JedisConnectionConfiguration是不会进行注入的。

序列化

尽管我们在redis数据库输入汉字,显示乱码,但是经过RedisTemplate拿到的数据是正常的。这是由于RedisTemplate帮我们做了序列化处理(默认为jdk序列化)。我们实现自定义RedisTemplate的时候可以对其做相应的修改。

QQ截图20220117005538

  • 序列化方式

    • OxmSerializer
      以xml格式存储(但还是String类型~),解析起来也比较复杂,效率也比较低。

    • JdkSerializationRedisSerializer
      从源码里可以看出,这是RestTemplate类默认的序列化方式。

    • StringRedisSerializer
      也是StringRedisTemplate默认的序列化方式,key和value都会采用此方式进行序列化,是被推荐使用的,对开发者友好,轻量级,效率也比较高。

    • GenericToStringSerializer
      他需要调用者给传一个对象到字符串互转的Converter(相当于转换为字符串的操作交给转换器去做),使用起来其比较麻烦。所以不太推荐使用。

    • Jackson2JsonRedisSerializer
      从名字可以看出来,这是把一个对象以Json的形式存储,效率高且对调用者友好。

    • GenericJackson2JsonRedisSerializer
      基本和上面的Jackson2JsonRedisSerializer功能差不多,使用方式也差不多。

定制RedisTemplete

  • 使用redis自带的序列化定制(GenericJackson2JsonRedisSerializer推荐

        @Bean
        public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
            // 将template 泛型设置为 <String, Object>
            RedisTemplate<String, Object> template = new RedisTemplate();
            // 连接工厂,不必修改
            template.setConnectionFactory(redisConnectionFactory);
            /*
             * 序列化设置
             */
            // key、hash的key 采用 String序列化方式
            template.setKeySerializer(RedisSerializer.string());
            template.setHashKeySerializer(RedisSerializer.string());
            // value、hash的value 采用 Jackson 序列化方式
            template.setValueSerializer(RedisSerializer.json());
            template.setHashValueSerializer(RedisSerializer.json());
            template.afterPropertiesSet();
    
            return template;
        }
    
  • 使用Jackson2JsonRedisSerializer序列化来定制。

    优点是速度快,序列化后的字符串短小精悍,不需要实现Serializable接口。
    但缺点也非常致命:那就是此类的构造函数中有一个类型参数,必须提供要序列化对象的类型信息(.class对象)

     @Bean
        public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
            RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
            template.setConnectionFactory(factory);
            /*
             * 序列化设置
             */
            Jackson2JsonRedisSerializer jacksonSerializer = new Jackson2JsonRedisSerializer(Object.class);
            ObjectMapper om = new ObjectMapper();
            om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
            om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
                    ObjectMapper.DefaultTyping.NON_FINAL,
                    JsonTypeInfo.As.WRAPPER_ARRAY);
            jacksonSerializer.setObjectMapper(om);
            // key、hash的key 采用 String序列化方式
            template.setKeySerializer(RedisSerializer.string());
            template.setHashKeySerializer(RedisSerializer.string());
            // value、hash的value 采用 Jackson 序列化方式
            template.setValueSerializer(jacksonSerializer);
            template.setHashValueSerializer(jacksonSerializer);
            template.afterPropertiesSet();
            return template;
        }
    

RedisUtils

@Component
public final class RedisUtil {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    // =============================common============================

    /**
     * 指定缓存失效时间
     *
     * @param key  键
     * @param time 时间(秒)
     */
    public boolean expire(String key, long time, TimeUnit timeUnit) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, timeUnit);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据key 获取过期时间
     *
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }


    /**
     * 判断key是否存在
     *
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 删除缓存
     *
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));
            }
        }
    }


    // ============================String=============================

    /**
     * 普通缓存获取
     *
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /**
     * 普通缓存放入
     *
     * @param key   键
     * @param value 值
     * @return true成功 false失败
     */

    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 普通缓存放入并设置时间
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */

    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 递增
     *
     * @param key   键
     * @param delta 要增加几(大于0)
     */
    public long incr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }


    /**
     * 递减
     *
     * @param key   键
     * @param delta 要减少几(小于0)
     */
    public long decr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }


    // ================================Map=================================

    /**
     * HashGet
     *
     * @param key  键 不能为null
     * @param item 项 不能为null
     */
    public Object hget(String key, String item) {
        return redisTemplate.opsForHash().get(key, item);
    }

    /**
     * 获取hashKey对应的所有键值
     *
     * @param key 键
     * @return 对应的多个键值
     */
    public Map<Object, Object> hmget(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * HashSet
     *
     * @param key 键
     * @param map 对应多个键值
     */
    public boolean hmset(String key, Map<String, Object> map) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * HashSet 并设置时间
     *
     * @param key  键
     * @param map  对应多个键值
     * @param time 时间(秒)
     * @return true成功 false失败
     */
    public boolean hmset(String key, Map<String, Object> map, long time, TimeUnit timeUnit) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            if (time > 0) {
                expire(key, time, timeUnit);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @param time  时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value, long time, TimeUnit timeUnit) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            if (time > 0) {
                expire(key, time, timeUnit);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 删除hash表中的值
     *
     * @param key  键 不能为null
     * @param item 项 可以使多个 不能为null
     */
    public void hdel(String key, Object... item) {
        redisTemplate.opsForHash().delete(key, item);
    }


    /**
     * 判断hash表中是否有该项的值
     *
     * @param key  键 不能为null
     * @param item 项 不能为null
     * @return true 存在 false不存在
     */
    public boolean hHasKey(String key, String item) {
        return redisTemplate.opsForHash().hasKey(key, item);
    }


    /**
     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
     *
     * @param key  键
     * @param item 项
     * @param by   要增加几(大于0)
     */
    public double hincr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, by);
    }


    /**
     * hash递减
     *
     * @param key  键
     * @param item 项
     * @param by   要减少记(小于0)
     */
    public double hdecr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, -by);
    }


    // ============================set=============================

    /**
     * 根据key获取Set中的所有值
     *
     * @param key 键
     */
    public Set<Object> sGet(String key) {
        try {
            return redisTemplate.opsForSet().members(key);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 根据value从一个set中查询,是否存在
     *
     * @param key   键
     * @param value 值
     * @return true 存在 false不存在
     */
    public boolean sHasKey(String key, Object value) {
        try {
            return redisTemplate.opsForSet().isMember(key, value);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 将数据放入set缓存
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSet(String key, Object... values) {
        try {
            return redisTemplate.opsForSet().add(key, values);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 将set数据放入缓存
     *
     * @param key    键
     * @param time   时间(秒)
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSetAndTime(String key, long time, TimeUnit timeUnit, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().add(key, values);
            if (time > 0) {
                expire(key, time, timeUnit);
            }
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 获取set缓存的长度
     *
     * @param key 键
     */
    public long sGetSetSize(String key) {
        try {
            return redisTemplate.opsForSet().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 移除值为value的
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 移除的个数
     */

    public long setRemove(String key, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().remove(key, values);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    // ===============================list=================================

    /**
     * 获取list缓存的内容
     *
     * @param key   键
     * @param start 开始
     * @param end   结束 0 到 -1代表所有值
     */
    public List<Object> lGet(String key, long start, long end) {
        try {
            return redisTemplate.opsForList().range(key, start, end);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 获取list缓存的长度
     *
     * @param key 键
     */
    public long lGetListSize(String key) {
        try {
            return redisTemplate.opsForList().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 通过索引 获取list中的值
     *
     * @param key   键
     * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
     */
    public Object lGetIndex(String key, long index) {
        try {
            return redisTemplate.opsForList().index(key, index);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     */
    public boolean lSet(String key, Object value) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     */
    public boolean lSet(String key, Object value, long time,TimeUnit timeUnit) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            if (time > 0) {
                expire(key, time,timeUnit);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

    }


    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, List<Object> value) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

    }


    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     * @return
     */
    public boolean lSet(String key, List<Object> value, long time,TimeUnit timeUnit) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            if (time > 0) {
                expire(key, time,timeUnit);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 根据索引修改list中的某条数据
     *
     * @param key   键
     * @param index 索引
     * @param value 值
     * @return
     */

    public boolean lUpdateIndex(String key, long index, Object value) {
        try {
            redisTemplate.opsForList().set(key, index, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 移除N个值为value
     *
     * @param key   键
     * @param count 移除多少个
     * @param value 值
     * @return 移除的个数
     */

    public long lRemove(String key, long count, Object value) {
        try {
            Long remove = redisTemplate.opsForList().remove(key, count, value);
            return remove;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }

    }

    // ===============================HyperLogLog=================================

    public long pfadd(String key, String value) {
        return redisTemplate.opsForHyperLogLog().add(key, value);
    }

    public long pfcount(String key) {
        return redisTemplate.opsForHyperLogLog().size(key);
    }

    public void pfremove(String key) {
        redisTemplate.opsForHyperLogLog().delete(key);
    }

    public void pfmerge(String key1, String key2) {
        redisTemplate.opsForHyperLogLog().union(key1, key2);
    }


}

Redis.conf详解

开头

# units are case insensitive so 1GB 1Gb 1gB are all the same.
单位不区分大小写

包含

# include /path/to/local.conf
# include /path/to/other.conf
`include` :可包含多个配置文件`

网络

bind 127.0.0.1 -::1
`bind` :redis服务绑定在本机的网卡(IP)上,bind后面的ip地址只能是ifconfig的IP地址或,这里是一大误点,我们并不能用bind绑定的ip限定请求访问,很多redis无法启动的原因就是这个,尤其是redis6.0后台启动不会有提示。很容易以为服务器的redis已启动。我们限制ip访问的手段一般是防火墙和安全组。使用远程连接我们可以配置bind 0.0.0.0或bind 服务器ip或者直接注释。

protected-mode yes
`protected-mode` :保护模式开关,而启动保护模式的条件不止这一个。protected-mode yes && 没有bind指令 && 没有设置密码。启动保护模式后只有本机才可以访问redis。

port 6379
`port` :设置端口号。

tcp-backlog 511
`tcp-backlog` :设置tcp的backlog,backlog其实是一个连接队列,backlog队列总和 = 未完成三次握手队列 + 已经完成三次握手队列。在高并发环境下需要一个高backlog值来避免慢客户端连接问题。注意Linux内核会将这个值减小到/proc/sys/net/core/somaxconn的值,所以需要确认增大somaxconn和tcp_max_syn_backlog两个值来达到想要的效果。

timeout 0
`timeout` :设置客户端连接时的超时时间,单位为秒。当客户端在这段时间内没有发出任何指令,那么关闭该连接。默认值为0,表示不关闭。

tcp-keepalive 300
`tcp-keepalive` :单位是秒,表示将周期性的使用SO_KEEPALIVE检测客户端是否还处于健康状态,避免服务器一直阻塞,官方给出的建议值是300s,如果设置为0,则不会周期性的检测。

通用配置

daemonize yes
`daemonize` :是否以守护线程开启,默认是no

pidfile /var/run/redis_6379.pid
`pidfile` :配置PID文件路径,当redis作为守护进程运行的时候,它会把 pid 默认写到 
/var/redis/run/redis_6379.pid 文件里面

loglevel notice
`loglevel` :定义日志级别。默认值为notice,有如下4种取值:
    debug(记录大量日志信息,适用于开发、测试阶段)
     verbose(较多日志信息)
     notice(适量日志信息,使用于生产环境)
     warning(仅有部分重要、关键信息才会被记录)
     
logfile ""
`logfile` :配置log文件地址,默认打印在命令行终端的窗口上

# syslog-enabled no
`Syslog-enabled` :是否把日志输出到syslog中

# syslog-ident redis
`Syslog-ident` :指定syslog里的日志标志

# syslog-facility local0
`Syslog-facility` :指定syslog设备,可以是USER或LOCAL0 - LOCAL7

databases 16
`databases` :设置数据库的数目。默认的数据库是DB 0 ,可以在每个连接上使用select <dbid>  命令选择一个不同的数据库,dbid是一个介于0到databases - 1 之间的数值。默认值是 16,也就是说默认Redis有16个数据库。

always-show-logo no
`always-show-logo` :是否总是显示logo

快照

# save 3600 1
# save 300 100
# save 60 10000
`save` :在指定时间内有一定次数的写入,进行持久化操作

stop-writes-on-bgsave-error yes
`stop-writes-on-bgsave-error` :默认值为yes。当启用了RDB且最后一次后台保存数据失败,Redis是否停止接收数据。这会让用户意识到数据没有正确持久化到磁盘上,否则没有人会注意到灾难(disaster)发生了。如果Redis重启了,那么又可以重新开始接收数据了

rdbcompression yes
`rdbcompression` :默认值是yes。对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis会采用LZF算法进行压缩。如果你不想消耗CPU来进行压缩的话,可以设置为关闭此功能,但是存储在磁盘上的快照会比较大。

rdbchecksum yes
`rdbchecksum` :默认值是yes。在存储快照后,我们还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。

dbfilename dump.rdb
`dbfilename` :设置快照的文件名,默认是 dump.rdb

dir ./
`dir`:设置快照文件的存放路径

主从复制

replica-serve-stale-data yes
`slave-serve-stale-data` :默认值为yes。当一个 slave 与 master 失去联系,或者复制正在进行的时候,slave 可能会有两种表现:
	如果为 yes ,slave 仍然会应答客户端请求,但返回的数据可能是过时,或者数据可能是空的在第一次同步的时候 
	如果为 no ,在你执行除了 info he salveof 之外的其他命令时,slave 都将返回一个 "SYNC with master in progress" 的错误
	
repl-diskless-sync no
`repl-diskless-sync` :主从数据复制是否使用无硬盘复制功能。默认值为no。

repl-diskless-sync-delay 5
`repl-diskless-sync-delay` :当启用无硬盘备份,服务器等待一段时间后才会通过套接字向从站传送RDB文件,这个等待时间是可配置的。  这一点很重要,因为一旦传送开始,就不可能再为一个新到达的从站服务。从站则要排队等待下一次RDB传送。因此服务器等待一段  时间以期更多的从站到达。延迟时间以秒为单位,默认为5秒。要关掉这一功能,只需将它设置为0秒,传送会立即启动。默认值为5。

repl-disable-tcp-nodelay no
`repl-disable-tcp-nodelay` :同步之后是否禁用从站上的TCP_NODELAY 如果你选择yes,redis会使用较少量的TCP包和带宽向从站发送数据。但这会导致在从站增加一点数据的延时。  Linux内核默认配置情况下最多40毫秒的延时。如果选择no,从站的数据延时不会那么多,但备份需要的带宽相对较多。默认情况下我们将潜在因素优化,但在高负载情况下或者在主从站都跳的情况下,把它切换为yes是个好主意。默认值为no。

安全

# requirepass foobared
`requirepass` :设置redis连接密码

# Command renaming (DEPRECATED)
`rename-command` :命令重命名,对于一些危险命令例如:
    flushdb(清空数据库)
    flushall(清空所有记录)
    config(客户端连接后可配置服务器)
    keys(客户端连接后可查看所有存在的键)                   

客户端

# maxclients 10000
`maxclients` :设置客户端最大并发连接数,默认无限制,Redis可以同时打开的客户端连接数为Redis进程可以打开的最大文件。  描述符数-32(redis server自身会使用一些),如果设置 maxclients为0 。表示不作限制。当客户端连接数到达限制时,Redis会关闭新的连接并向客户端返回max number of clients reached错误信息

内存管理

# maxmemory <bytes>
`maxmemory` :设置最大内存容量

# maxmemory-policy noeviction
`maxmemory-policy` :内存到达上限的处理策略
	volatile-lru:只对设置了过期时间的key进行LRU(默认值)
	allkeys-lru : 删除LRU算法的key
	volatile-lfu:只对设置了过期时间的key进行LFU(默认值)
	allkeys-lfu : 删除LRU算法的key
	volatile-random:随机删除即将过期key
	allkeys-random:随机删除
	volatile-ttl : 删除即将过期的
	noeviction : 永不过期,返回错误
	
# maxmemory-samples 5
`maxmemory-samples` :LRU 和 minimal TTL 算法都不是精准的算法,但是相对精确的算法(为了节省内存)。随意你可以选择样本大小进行检,redis默认选择3个样本进行检测,你可以通过maxmemory-samples进行设置样本数。

仅附加模式

appendonly no
`appendonly` :默认redis使用的是rdb方式持久化,这种方式在许多应用中已经足够用了。但是redis如果中途宕机,会导致可能有几分钟的数据丢失,根据save来策略进行持久化,Append Only File是另一种持久化方式,  可以提供更好的持久化特性。Redis会把每次写入的数据在接收后都写入appendonly.aof文件,每次启动时Redis都会先把这个文件的数据读入内存里,先忽略RDB文件。默认值为no。

appendfilename "appendonly.aof"
`appendfilename` :aof文件名,默认是"appendonly.aof"

appendfsync everysec
`appendfsync` :aof持久化策略的配置;no表示不执行fsync,由操作系统保证数据同步到磁盘,速度最快;always表示每次写入都执行fsync,以保证数据同步到磁盘;everysec表示每秒执行一次fsync,可能会导致丢失这1s数据

no-appendfsync-on-rewrite no
`no-appendfsync-on-rewrite` :在aof重写或者写入rdb文件的时候,会执行大量IO,此时对于everysec和always的aof模式来说,执行fsync会造成阻塞过长时间,no-appendfsync-on-rewrite字段设置为默认设置为no。如果对延迟要求很高的应用,这个字段可以设置为yes,否则还是设置为no,这样对持久化特性来说这是更安全的选择。   设置为yes表示rewrite期间对新写操作不fsync,暂时存在内存中,等rewrite完成后再写入,默认为no,建议yes。Linux的默认fsync策略是30秒。可能丢失30秒数据。默认值为no。

auto-aof-rewrite-percentage 100
`auto-aof-rewrite-percentage` :默认值为100。aof自动重写配置,当目前aof文件大小超过上一次重写的aof文件大小的百分之多少进行重写,即当aof文件增长到一定大小的时候,Redis能够调用bgrewriteaof对日志文件进行重写。当前AOF文件大小是上次日志重写得到AOF文件大小的二倍(设置为100)时,自动启动新的日志重写过程。

auto-aof-rewrite-min-size 64mb
`auto-aof-rewrite-min-size` :64mb。设置允许重写的最小aof文件大小,避免了达到约定百分比但尺寸仍然很小的情况还要重写。

aof-load-truncated yes
`aof-load-truncated` :aof文件可能在尾部是不完整的,当redis启动的时候,aof文件的数据被载入内存。重启可能发生在redis所在的主机操作系统宕机后,尤其在ext4文件系统没有加上data=ordered选项,出现这种现象  redis宕机或者异常终止不会造成尾部不完整现象,可以选择让redis退出,或者导入尽可能多的数据。如果选择的是yes,当截断的aof文件被导入的时候,会自动发布一个log给客户端然后load。如果是no,用户必须手动redis-check-aof修复AOF文件才可以。默认值为 yes。

Lua脚本

lua-time-limit 5000
`lua-time-limit` :一个lua脚本执行的最大时间,单位为ms。默认值为5000.

Redis群集

# cluster-enabled yes
`cluster-enabled` :集群开关,默认是不开启集群模式。

# cluster-config-file nodes-6379.conf
`cluster-config-file` :集群配置文件的名称,每个节点都有一个集群相关的配置文件,持久化保存集群的信息。 这个文件并不需要手动配置,这个配置文件有Redis生成并更新,每个Redis集群节点需要一个单独的配置文件。请确保与实例运行的系统中配置文件名称不冲突。默认配置为nodes-6379.conf。

# cluster-node-timeout 15000
`cluster-node-timeout` :可以配置值为15000。节点互连超时的阀值,集群节点超时毫秒数

# cluster-replica-validity-factor 10
`cluster-slave-validity-factor` :可以配置值为10。在进行故障转移的时候,全部slave都会请求申请为master,但是有些slave可能与master断开连接一段时间了,  导致数据过于陈旧,这样的slave不应该被提升为master。该参数就是用来判断slave节点与master断线的时间是否过长。判断方法是:比较slave断开连接的时间和(node-timeout * slave-validity-factor) + repl-ping-slave-period     如果节点超时时间为三十秒, 并且slave-validity-factor为10,假设默认的repl-ping-slave-period是10秒,即如果超过310秒slave将不会尝试进行故障转移

# cluster-migration-barrier 1
`cluster-migration-barrier` :可以配置值为1。master的slave数量大于该值,slave才能迁移到其他孤立master上,如这个参数若被设为2,那么只有当一个主节点拥有2 个可工作的从节点时,它的一个从节点会尝试迁移。

# cluster-require-full-coverage yes
`cluster-require-full-coverage` :默认情况下,集群全部的slot有节点负责,集群状态才为ok,才能提供服务。  设置为no,可以在slot没有全部分配的时候提供服务。不建议打开该配置,这样会造成分区的时候,小分区的master一直在接受写请求,而造成很长时间数据不一致。

Redis持久化

RDB

在指定时间间隔后,将内存中的数据集快照写入数据库 ;在恢复时候,直接读取快照文件,进行数据的恢复 ;

默认情况下, Redis 将数据库快照保存在名字为 dump.rdb的二进制文件中。文件名可以在配置文件中进行自定义

QQ截图20220118232926

  • 原理(bgsave)

    在进行 RDB 的时候,redis 的主线程是不会做 io 操作的,主线程会 fork 一个子线程来完成该操作;

    • Redis 调用forks。同时拥有父进程和子进程。
    • 子进程将数据集写入到一个临时 RDB 文件中。
    • 当子进程完成对新 RDB 文件的写入时,Redis 用新 RDB 文件替换原来的 RDB 文件,并删除旧的 RDB 文件。

    这种工作方式使得 Redis 可以从写时复制(copy-on-write)机制中获益(因为是使用子进程进行写操作,而父进程依然可以接收来自客户端的请求。

    o_210624033259r5

  • 触发机制

    • 手动触发

      • save命令

        当客户端向Redis server发送save命令请求进行持久化时,由于Redis是用一个主线程来处理所有,save命令会阻塞Redis server处理其他客户端的请求,直到数据同步完成。数据量大的话会造成长时间的阻塞,线上一般禁止使用。

      • bgsave命令

        与save命令不同,bgsave是异步执行的,当执行bgsave命令之后,Redis主进程会fork 一个子进程将数据保存到rdb文件中,同步完数据之后,对原有文件进行替换,然后通知主进程表示同步完成。主进程阻塞时间只有fork阶段的那一下。相对于save,阻塞时间很短。

      • 对比

        命令 save bgsave
        IO类型 同步 异步
        阻塞 是(阻塞发生在fock(),通常非常快)
        复杂度 O(n) O(n)
        优点 不会消耗额外的内存 不阻塞客户端命令
        缺点 阻塞客户端命令 需要fock子进程,消耗内存
    • 自动触发

      • 配置redis.conf触发规则,自动执行

        save :这里是用来配置触发 Redis的 RDB 持久化条件,也就是什么时候将内存中的数据保存到硬盘。比如save m n。表示 m 秒内数据集存在 n 次修改时,自动触发 bgsave。

      • shutdowm:执行shutdown命令关闭服务器时,根据服务器的持久化配置选项,决定是否执行数据保存操作:

        • 如果服务器启用了RDB持久化功能或没有启用任何持久化功能,并且数据库距离最后一次成功创建RDB文件之后已经发生了改变,那么服务器将执行SAVE命令,创建 一个新的RDB文件。
        • 如果服务器启用了AOF持久化功能或者RDB-AOF混合持久化功能,那么它将冲洗AOF文件,确保所有已执行的命令都被记录到了AOF文件中。
        • 如果服务器同时启用RDB持久化功能和AOF持久化功能,那么它将冲洗AOF文件,确保所有已执行的命令都被记录到了AOF文件中,然后执行SAVE命令。
      • flushall:Redis Flushall 命令用于清空整个 Redis 服务器的数据(删除所有数据库的所有 key 。在执行这个命令过程中,会触发自动持久化,把 RDB 文件清空。)

      • 主从复制:如果从节点执行全量复制操作,主节点自动执行BGSAVE生成RDB文件并发送给从节点。

  • 优缺点

    • 优点
      • RDB 文件紧凑,全量备份,非常适合用于进行备份和灾难恢复。
      • 生成 RDB 文件的时候,Redis 主进程会 fork 一个子进程来处理所有保存工作,主进程不需要进行任何磁盘 IO 操作。
      • Redis加载RDB文件的速度比AOF快很多,因为RDB文件中直接存储的时内存数据,而AOF文件中存储的是一条条命令,需要重演命令。
    • 缺点
      • RDB无法做到实时持久化,若在两次bgsave间宕机,则会丢失区间(分钟级)的增量数据,不适用于实时性要求较高的场景
      • RDB的cow机制中,fork子进程属于重量级操作(占内存),并且会阻塞redis主进程
      • 存在老版本的Redis不兼容新版本RDB格式文件的问题
  • 配置

    # save 3600 1
    # save 300 100
    # save 60 10000
    `save` :在指定时间内有一定次数的写入,进行持久化操作
    
    stop-writes-on-bgsave-error yes
    `stop-writes-on-bgsave-error` :默认值为yes。当启用了RDB且最后一次后台保存数据失败,Redis是否停止接收数据。这会让用户意识到数据没有正确持久化到磁盘上,否则没有人会注意到灾难(disaster)发生了。如果Redis重启了,那么又可以重新开始接收数据了
    
    rdbcompression yes
    `rdbcompression` :默认值是yes。对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis会采用LZF算法进行压缩。如果你不想消耗CPU来进行压缩的话,可以设置为关闭此功能,但是存储在磁盘上的快照会比较大。
    
    rdbchecksum yes
    `rdbchecksum` :默认值是yes。在存储快照后,我们还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。
    
    dbfilename dump.rdb
    `dbfilename` :设置快照的文件名,默认是 dump.rdb
    
    dir ./
    `dir`:设置快照文件的存放路径
    

AOF

快照功能(RDB)并不是非常耐久(durable): 如果 Redis 因为某些原因而造成故障停机, 那么服务器将丢失最近写入、且仍未保存到快照中的那些数据。 从 1.1 版本开始, Redis 增加了一种完全耐久的持久化方式: AOF 持久化。你可以在配置文件中打开AOF方式:appendonly yes

打开AOF后, 每当 Redis 执行一个改变数据集的命令时(比如 SET), 这个命令就会被追加到 AOF 文件的末尾。这样的话, 当 Redis 重新启动时, 程序就可以通过重新执行 AOF 文件中的命令来达到重建数据集的目的。

QQ截图20220118232939

  • 策略

    • always

      每次有新命令追加到 AOF 文件时就执行一次 fsync :非常慢,也非常安全。

    • everysec

      每秒 fsync 一次:足够快(和使用 RDB 持久化差不多),并且在故障时只会丢失 1 秒钟的数据。推荐(并且也是默认)的措施为每秒 fsync 一次, 这种 fsync 策略可以兼顾速度和安全性。

    • no

      从不 fsync :将数据交给操作系统来处理,由操作系统来决定什么时候同步数据。更快,也更不安全的选择。

    • 对比

      命令 优点 缺点
      always 不丢失数据 IO开销大,一般SATA磁盘只有几百TPS
      everysec 每秒进行与fsync,最多丢失1秒数据 可能丢失1秒数据
      no 不用管 不可控
  • 修复文件

    • redis-check-aof --fix appendonly.aof
      
  • 重写

    因为 AOF 的运作方式是不断地将命令追加到文件的末尾, 所以随着写入命令的不断增加, AOF 文件的体积也会变得越来越大。举个例子, 如果你对一个计数器调用了 100 次 INCR , 那么仅仅是为了保存这个计数器的当前值, AOF 文件就需要使用 100 条记录(entry)。然而在实际上, 只使用一条 SET 命令已经足以保存计数器的当前值了, 其余 99 条记录实际上都是多余的。
    为了处理这种情况, Redis 支持一种有趣的特性: 可以在不打断服务客户端的情况下, 对 AOF 文件进行重建(rebuild)。执行 bgrewriteaof 命令, Redis 将生成一个新的 AOF 文件, 这个文件包含重建当前数据集所需的最少命令。
    Redis 2.2 需要自己手动执行 bgrewriteaof 命令; Redis 2.4 则可以通过配置自动触发 AOF 重写。

    • 实现方式

      • bgrewriteaof 命令

        Redis bgrewriteaof 命令用于异步执行一个 AOF(AppendOnly File)文件重写操作。重写会创建一个当前AOF文件的体积优化版本。
        即使 bgrewriteaof 执行失败,也不会有任何数据丢失,因为旧的AOF文件在 bgrewriteaof 成功之前不会被修改。
        AOF 重写由 Redis 自行触发,bgrewriteaof 仅仅用于手动触发重写操作。
        具体内容:

        • 如果一个子Redis是通过磁盘快照创建的,AOF重写将会在RDB终止后才开始保存。这种情况下BGREWRITEAOF任然会返回OK状态码。从Redis 2.6起你可以通过INFO命令查看AOF重写执行情况。
        • 如果只在执行的AOF重写返回一个错误,AOF重写将会在稍后一点的时间重新调用。
      • AOF重写配置

        auto-aof-rewrite-min-size 64mb
        auto-aof-rewrite-percentage 100
        

        当AOF文件的体积大于64Mb,并且AOF文件的体积比上一次重写之久的体积大了至少一倍(100%)时,Redis将执行 bgrewriteaof 命令进行重写。

        o_210624033403r14

  • 优缺点

    • 优点
      • 使用AOF 会让你的Redis更加耐久: 你可以使用不同的fsync策略:无fsync,每秒fsync,每次写的时候fsync。使用默认的每秒fsync策略,Redis的性能依然很好(fsync是由后台线程进行处理的,主线程会尽力处理客户端请求),一旦出现故障,你最多丢失1秒的数据。
      • AOF文件是一个只进行追加的日志文件,所以不需要写入seek,即使由于某些原因(磁盘空间已满,写的过程中宕机等等)未执行完整的写入命令,你也也可使用redis-check-aof工具修复这些问题。
      • Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写: 重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。 整个重写操作是绝对安全的,因为 Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生停机,现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作。
      • AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析(parse)也很轻松。 导出(export) AOF 文件也非常简单: 举个例子, 如果你不小心执行了 FLUSHALL 命令, 但只要 AOF 文件未被重写, 那么只要停止服务器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态。
    • 缺点
      • 对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。
      • 根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB 。 在一般情况下, 每秒 fsync 的性能依然非常高, 而关闭 fsync 可以让 AOF 的速度和 RDB 一样快, 即使在高负荷之下也是如此。 不过在处理巨大的写入载入时,RDB 可以提供更有保证的最大延迟时间(latency)。
  • 配置

    appendonly no
    `appendonly` :默认redis使用的是rdb方式持久化,这种方式在许多应用中已经足够用了。但是redis如果中途宕机,会导致可能有几分钟的数据丢失,根据save来策略进行持久化,Append Only File是另一种持久化方式,  可以提供更好的持久化特性。Redis会把每次写入的数据在接收后都写入appendonly.aof文件,每次启动时Redis都会先把这个文件的数据读入内存里,先忽略RDB文件。默认值为no。
    
    appendfilename "appendonly.aof"
    `appendfilename` :aof文件名,默认是"appendonly.aof"
    
    appendfsync everysec
    `appendfsync` :aof持久化策略的配置;no表示不执行fsync,由操作系统保证数据同步到磁盘,速度最快;always表示每次写入都执行fsync,以保证数据同步到磁盘;everysec表示每秒执行一次fsync,可能会导致丢失这1s数据
    
    no-appendfsync-on-rewrite no
    `no-appendfsync-on-rewrite` :在aof重写或者写入rdb文件的时候,会执行大量IO,此时对于everysec和always的aof模式来说,执行fsync会造成阻塞过长时间,no-appendfsync-on-rewrite字段设置为默认设置为no。如果对延迟要求很高的应用,这个字段可以设置为yes,否则还是设置为no,这样对持久化特性来说这是更安全的选择。   设置为yes表示rewrite期间对新写操作不fsync,暂时存在内存中,等rewrite完成后再写入,默认为no,建议yes。Linux的默认fsync策略是30秒。可能丢失30秒数据。默认值为no。
    
    auto-aof-rewrite-percentage 100
    `auto-aof-rewrite-percentage` :默认值为100。aof自动重写配置,当目前aof文件大小超过上一次重写的aof文件大小的百分之多少进行重写,即当aof文件增长到一定大小的时候,Redis能够调用bgrewriteaof对日志文件进行重写。当前AOF文件大小是上次日志重写得到AOF文件大小的二倍(设置为100)时,自动启动新的日志重写过程。
    
    auto-aof-rewrite-min-size 64mb
    `auto-aof-rewrite-min-size` :64mb。设置允许重写的最小aof文件大小,避免了达到约定百分比但尺寸仍然很小的情况还要重写。
    
    aof-load-truncated yes
    `aof-load-truncated` :aof文件可能在尾部是不完整的,当redis启动的时候,aof文件的数据被载入内存。重启可能发生在redis所在的主机操作系统宕机后,尤其在ext4文件系统没有加上data=ordered选项,出现这种现象  redis宕机或者异常终止不会造成尾部不完整现象,可以选择让redis退出,或者导入尽可能多的数据。如果选择的是yes,当截断的aof文件被导入的时候,会自动发布一个log给客户端然后load。如果是no,用户必须手动redis-check-aof修复AOF文件才可以。默认值为 yes。
    

总结

  1. RDB 持久化方式能够在指定的时间间隔内对你的数据进行快照存储
  2. AOF 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始
    的数据,AOF命令以Redis 协议追加保存每次写的操作到文件末尾,Redis还能对AOF文件进行后台重
    写,使得AOF文件的体积不至于过大。
  3. 只做缓存,如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化
  4. 同时开启两种持久化方式
    在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF
    文件保存的数据集要比RDB文件保存的数据集要完整。
    RDB 的数据不实时,同时使用两者时服务器重启也只会找AOF文件,那要不要只使用AOF呢?作者
    建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份),快速重启,而且不会有
    AOF可能潜在的Bug,留着作为一个万一的手段。
  5. 性能建议
    因为RDB文件只用作后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就够
    了,只保留 save 900 1 这条规则。
    如果Enable AOF ,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自
    己的AOF文件就可以了,代价一是带来了持续的IO,二是AOF rewrite 的最后将 rewrite 过程中产
    生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少AOF rewrite
    的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上,默认超过原大小100%大小重
    写可以改到适当的数值。
    如果不Enable AOF ,仅靠 Master-Slave Repllcation 实现高可用性也可以,能省掉一大笔IO,也
    减少了rewrite时带来的系统波动。代价是如果Master/Slave 同时倒掉,会丢失十几分钟的数据,
    启动脚本也要比较两个 Master/Slave 中的 RDB文件,载入较新的那个,微博就是这种架构。

Redis发布订阅

20200513215523258

20200513215523258

  • 命令

    命令 描述
    PSUBSCRIBE pattern [pattern..] 订阅一个或多个符合给定模式的频道。
    PUNSUBSCRIBE pattern [pattern..] 退订一个或多个符合给定模式的频道。
    PUBSUB subcommand [argument[argument]] 查看订阅与发布系统状态。
    PUBLISH channel message 向指定频道发布消息
    SUBSCRIBE channel [channel..] 订阅给定的一个或多个频道。
    SUBSCRIBE channel [channel..] 退订一个或多个频道
  • 原理

    每个 Redis 服务器进程都维持着一个表示服务器状态的 redis.h/redisServer 结构, 结构的 pubsub_channels 属性是一个字典, 这个字典就用于保存订阅频道的信息,其中,字典的键为正在被订阅的频道, 而字典的值则是一个链表, 链表中保存了所有订阅这个频道的客户端。

    客户端订阅,就被链接到对应频道的链表的尾部,退订则就是将客户端节点从链表中移除。

    2020051321554964

  • 缺点

    • 客户端订阅频道后,自身读取消息的速度却不够快的话,那么不断积压的消息会使redis输出缓冲区的体积变得越来越大,这可能使得redis本身的速度变慢,甚至直接崩溃。
    • 数据传输可靠性不足,如果在订阅方断线,那么他将会丢失所有在短线期间发布者发布的消息。
  • 应用

    • 消息订阅:公众号订阅,微博关注等等(起始更多是使用消息队列来进行实现)
    • 多人在线聊天室。

Redis集群

  • 单台服务器难以负载大量的请求
  • 单台服务器故障率高,系统崩坏概率大
  • 单台服务器内存容量有限。

Redis主从复制

主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(Master/Leader),后者称为从节点(Slave/Follower), 数据的复制是单向的!只能由主节点复制到从节点(主节点以写为主、从节点以读为主)。

默认情况下,每台Redis服务器都是主节点,一个主节点可以有0个或者多个从节点,但每个从节点只能由一个主节点。

  • 作用

    • 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余的方式。
    • 故障恢复:当主节点故障时,从节点可以暂时替代主节点提供服务,是一种服务冗余的方式
    • 负载均衡:在主从复制的基础上,配合读写分离,由主节点进行写操作,从节点进行读操作,分担服务器的负载;尤其是在多读少写的场景下,通过多个从节点分担负载,提高并发量。
    • 高可用基石:主从复制还是哨兵和集群能够实施的基础
  • 配置

    • 查看主从配置

      127.0.0.1:6379> info replication(无需redis-cli)或/java/redis5/bin/redis-cli info replication

    • 基本配置

      replica-serve-stale-data yes
      `slave-serve-stale-data` :默认值为yes。当一个 slave 与 master 失去联系,或者复制正在进行的时候,slave 可能会有两种表现:
      	如果为 yes ,slave 仍然会应答客户端请求,但返回的数据可能是过时,或者数据可能是空的在第一次同步的时候 
      	如果为 no ,在你执行除了 info he salveof 之外的其他命令时,slave 都将返回一个 "SYNC with master in progress" 的错误
      
      slave-read-only yes
      `slave-read-only` :配置Redis的Slave实例是否接受写操作,即Slave是否为只读Redis。默认值为yes。	
      	
      repl-diskless-sync no
      `repl-diskless-sync` :主从数据复制是否使用无硬盘复制功能。默认值为no。
      
      repl-diskless-sync-delay 5
      `repl-diskless-sync-delay` :当启用无硬盘备份,服务器等待一段时间后才会通过套接字向从站传送RDB文件,这个等待时间是可配置的。  这一点很重要,因为一旦传送开始,就不可能再为一个新到达的从站服务。从站则要排队等待下一次RDB传送。因此服务器等待一段  时间以期更多的从站到达。延迟时间以秒为单位,默认为5秒。要关掉这一功能,只需将它设置为0秒,传送会立即启动。默认值为5。
      
      repl-disable-tcp-nodelay no
      `repl-disable-tcp-nodelay` :同步之后是否禁用从站上的TCP_NODELAY 如果你选择yes,redis会使用较少量的TCP包和带宽向从站发送数据。但这会导致在从站增加一点数据的延时。  Linux内核默认配置情况下最多40毫秒的延时。如果选择no,从站的数据延时不会那么多,但备份需要的带宽相对较多。默认情况下我们将潜在因素优化,但在高负载情况下或者在主从站都跳的情况下,把它切换为yes是个好主意。默认值为no。
      
    • 主从配置

      一般情况下,我们仅进行从机设置(redis启动默认主机)

      # replicaof <masterip> <masterport>
      `replicaof` :设置主机的ip和端口号。
      
      # masterauth <master-password>
      `masterauth`:若主机设置了密码,从机要设置相应的密码方可访问。
      

      题外话

      由于slaveof这个命令带有冒犯,redis5.0后作者被迫加入replicaof来代替,为保证旧版本的兼容,原命令依旧生效。配置项也变为replicaof <masterip> <masterport>

  • 单机多进程redis伪集群的配置修改

    • 端口号
    • pid文件名
    • 日志文件名
    • rdb文件名
  • 使用规则

    • 从机只能读,不能写,主机可读可写但是多用于写。

    • 主Redis宕机

      • 第一步:在从数据库中执行REPLICAOF NO ONE命令,断开主从关系并且提升为主库继续服务。
      • 第二步:将主库重新启动后,执行REPLICAOF命令,将其设置为其他库的从库,数据就能更新回来。
    • 从Redis宕机

      • Redis从库重新启动后会自动加入到主从架构中,自动完成同步数据;(从库在有做持久化的前提下实现增量复制)
    • 复制原理

      • Slave 启动成功连接到 master 后会发送一个sync同步命令
      • Master 接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行
        完毕之后,master将传送整个数据文件到slave,并完成一次完全同步。
      • 全量复制:而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。
      • 增量复制:Master 继续将新的所有收集到的修改命令依次传给slave,完成同步
  • 优缺点

    • 优点:

      • 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离
      • 为了分载Master的读操作压力,Slave服务器可以为客户端提供只读操作的服务,写服务仍然必须由Master来完成
      • Slave同样可以接受其它Slaves的连接和同步请求,这样可以有效的分载Master的同步压力。
      • Master Server是以非阻塞的方式为Slaves提供服务。所以在Master-Slave同步期间,客户端仍然可以提交查询或修改请求。
      • Slave Server同样是以非阻塞的方式完成数据同步。在同步期间,如果有客户端提交查询请求,Redis则返回同步之前的数据
    • 缺点:

      • Redis不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复。
      • 主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。
      • Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。

Redis哨兵模式

Redis主从模式虽然能做到很好的数据备份,但是他并不是高可用的。一旦主服务器点宕机后,只能通过人工去切换主服务器。因此Redis的哨兵模式也就是为了解决主从模式的高可用方案

哨兵模式引入了一个Sentinel系统去监视主服务器及其所属的所有从服务器。一旦发现有主服务器宕机后,会自动选举其中的一个从服务器升级为新主服务器以达到故障转义的目的。

同样的Sentinel系统也需要达到高可用,所以一般也是集群,互相之间也会监控。而Sentinel其实本身也是一个以特殊模式允许Redis服务器。

  • 配置

    # Example sentinel.conf
     
    # 哨兵sentinel实例运行的端口 默认26379
    port 26379
     
    # 哨兵sentinel的工作目录
    dir /tmp
     
    # 哨兵sentinel监控的redis主节点的 ip port 
    # master-name  可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。
    # quorum 当这些quorum个数sentinel哨兵认为master主节点失联 那么这时 客观上认为主节点失联了
    # sentinel monitor <master-name> <ip> <redis-port> <quorum>
    sentinel monitor mymaster 127.0.0.1 6379 1
     
    # 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码
    # 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码
    # sentinel auth-pass <master-name> <password>
    sentinel auth-pass mymaster MySUPER--secret-0123passw0rd
     
     
    # 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒
    # sentinel down-after-milliseconds <master-name> <milliseconds>
    sentinel down-after-milliseconds mymaster 30000
     
    # 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步,
    这个数字越小,完成failover所需的时间就越长,
    但是如果这个数字越大,就意味着越 多的slave因为replication而不可用。
    可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。
    # sentinel parallel-syncs <master-name> <numslaves>
    sentinel parallel-syncs mymaster 1
     
     
     
    # 故障转移的超时时间 failover-timeout 可以用在以下这些方面: 
    #1. 同一个sentinel对同一个master两次failover之间的间隔时间。
    #2. 当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时。
    #3.当想要取消一个正在进行的failover所需要的时间。  
    #4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了
    # 默认三分钟
    # sentinel failover-timeout <master-name> <milliseconds>
    sentinel failover-timeout mymaster 180000
     
    # SCRIPTS EXECUTION
     
    #配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。
    #对于脚本的运行结果有以下规则:
    #若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10
    #若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。
    #如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。
    #一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。
     
    #通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本,
    #这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,
    #一个是事件的类型,
    #一个是事件的描述。
    #如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无法正常启动成功。
    #通知脚本
    # sentinel notification-script <master-name> <script-path>
    sentinel notification-script mymaster /var/redis/notify.sh
     
    # 客户端重新配置主节点参数脚本
    # 当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。
    # 以下参数将会在调用脚本时传给脚本:
    # <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
    # 目前<state>总是“failover”,
    # <role>是“leader”或者“observer”中的一个。 
    # 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的
    # 这个脚本应该是通用的,能被多次调用,不是针对性的。
    # sentinel client-reconfig-script <master-name> <script-path>
    sentinel client-reconfig-script mymaster /var/redis/reconfig.sh
    
  • 启动

    • redis-sentinel xxx/sentinel.conf
      
  • 原理

    • Sentinel与主从服务器建立连接

      • Sentinel服务器启动之后便会创建于主服务器的命令连接并订阅主服务器的sentinel:hello频道以创建订阅连接
      • Sentinel默认会每10秒向主服务器发送 INFO 命令,主服务器则会返回主服务器本身的信息,以及其所有从服务器的信息。
      • 根据返回的信息,Sentinel服务器如果发现有新的从服务器上线后也会像连接主服务器时一样,向从服务器同时创建命令连接与订阅连接。
    • 判定主服务器是否下线

      每一个Sentinel服务器每秒会向其连接的所有实例包括主服务器,从服务器,其他Sentinel服务器)发送 PING命令,根据是否回复 PONG 命令来判断实例是否下线。

      判定主观下线

      如果实例在收到 PING命令的down-after-milliseconds毫秒内(根据配置),未有有效回复。则该实例将会被发起 PING命令的Sentinel认定为主观下线。

      判定客观下线

      当一台主服务器没某个Sentinel服务器判定为客观下线时,为了确保该主服务器是真的下线,Sentinel会向Sentinel集群中的其他的服务器确认,如果判定主服务器下线的Sentinel服务器达到一定数量时(一般是N/2+1),那么该主服务器将会被判定为客观下线,需要进行故障转移。

    • 选举领头Sentinel

      当有主服务器被判定客观下线后,Sentinel集群会选举出一个领头Sentinel服务器来对下线的主服务器进行故障转移操作。整个选举其实是基于RAFT一致性算法而实现的,大致的思路如下:

      • 每个发现主服务器下线的Sentinel都会要求其他Sentinel将自己设置为局部领头Sentinel。
      • 接收到的Sentinel可以同意或者拒绝
      • 如果有一个Sentinel得到了半数以上Sentinel的支持则在此次选举中成为领头Sentinel。
      • 如果给定时间内没有选举出领头Sentinel,那么会再一段时间后重新开始选举,直到选举出领头Sentinel。
    • 选举新的主服务器

      领头服务器会从从服务中挑选出一个最合适的作为新的主服务器。挑选的规则是:

      • 选择健康状态的从节点,排除掉断线的,最近没有回复过 INFO命令的从服务器。

      • 选择优先级配置高的从服务器

      • 选择复制偏移量大的服务器(表示数据最全)

      挑选出新的主服务器后,领头服务器将会向新主服务器发送 SLAVEOF no one命令将他真正升级为主服务器,并且修改其他从服务器的复制目标,将旧的主服务器设为从服务器,以此来达到故障转移。

  • 优缺点

    • 优点:
      • 哨兵集群,基于主从复制模式,所有主从复制的优点,它都有
      • 主从可以切换,故障可以转移,系统的可用性更好
      • 哨兵模式是主从模式的升级,手动到自动,更加健壮
    • 缺点:
      • Redis不好在线扩容,集群容量一旦达到上限,在线扩容就十分麻烦
      • 实现哨兵模式的配置其实是很麻烦的,里面有很多配置项

Redis-Cluster集群

Redis哨兵模式实现了高可用,读写分离,但是其主节点仍然只有一个,即写入操作都是在主节点中,这也成为了性能的瓶颈。

因此Redis在3.0后加入了Cluster模式,它采用去无心节点方式实现,集群将会通过分片方式保存数据库中的键值对

  • 节点

    • 一个Redis集群中会由多个节点组成,每个节点都是互相连接的,会保存自己与其他节点的信息。节点之间通过gossip协议交换互相的状态,以及保新加入的节点信息。
  • 数据的Sharding

    • Redis Cluster的整个数据库将会被分为16384个哈希槽,数据库中的每个键都属于这16384个槽中的其中一个,集群中的每个节点可以处0个或者最多16384个槽。
  • 设置槽指派

    • 通过命令 CLUSTER ADDSLOTS <slot> [slot...] 命令我们可以将一个或多个槽指派给某个节点。

      127.0.0.1:7777> CLUSTER ADDSLOTS 1 2 3 4 5 命令就是将1,2,3,4,5号插槽指派给本地端口号为7777的节点负责。

    • 设置后节点将会将槽指派的信息发送给其他集群,让其他集群更新信息。

  • Sharding流程

    • 当客户端发起对键值对的操作指令后,将任意分配给其中某个节点

    • 节点计算出该键值所属插槽

    • 判断当前节点是否为该键所属插槽

    • 如果是的话直接执行操作命令

    • 如果不是的话,向客户端返回moved错误,moved错误中将带着正确的节点地址与端口,客户端收到后可以直接转向至正确节点

  • Redis Cluster的高可用

    • Redis的每个节点都可以分为主节点与对应从节点。主节点负责处理槽,从节点负责复制某个主节点,并在主节点下线时,代替下线的主节点。

      QQ截图20220119200319

  • 如何实现故障转移

    其实与哨兵模式类似,Redis的每个节点都会定期向其他节点发送Ping消息,以此来检测对方是否在线。当一个节点检测到另一个节点下线后,会将其设置为疑似下线。如果一个机器中,有半数以上的节点将某个主节点设为疑似下线,则该节点将会被标记为已下线状态,并开始执行故障转移。

    • 通过raft算法从下线主节点的从节点中选出新的主节点
    • 被选中的从节点执行 SLAVEOF no one 命令,成为新的主节点
    • 新的主节点撤销掉已下线主节点的槽指派,并将这些槽指给自己
    • 新的主节点向集群中广播自己由从节点变为主节点
    • 新的主节点开始接受和负责自己处理槽的有关命令请求

缓存击穿、穿透与雪崩

  • 缓存和数据库数据一致性问题:

    • 分布式环境下非常容易出现缓存和数据库间数据一致性问题,针对这一点,如果项目对缓存的要求是强一致性的,那么就不要使用缓存。我们只能采取合适的策略来降低缓存和数据库间数据不一致的概率,而无法保证两者间的强一致性。合适的策略包括合适的缓存更新策略,更新数据库后及时更新缓存、缓存失败时增加重试机制。
  • 缓存穿透

    在默认情况下,用户请求数据时,会先在缓存(Redis)中查找,若没找到即缓存未命中,再在数据库中进行查找,数量少可能问题不大,可是一旦大量的请求数据(例如秒杀场景)缓存都没有命中的话,就会全部转移到数据库上,造成数据库极大的压力,就有可能导致数据库崩溃。网络安全中也有人恶意使用这种手段进行攻击被称为洪水攻击。

    • 解决方案

      • 布隆过滤器

        • 对所有可能查询的参数以Hash的形式存储,以便快速确定是否存在这个值,在控制层先进行拦截校验,校验不通过直接打回,减轻了存储系统的压力。

        QQ截图20220119210520

      • 空对象缓存

        • 一次请求若在缓存和数据库中都没找到,就在缓存中方一个空对象用于处理后续这个请求。
        • 缺陷:
          • 存储空对象也需要空间,大量的空对象会耗费一定的空间,存储效率并不高。解决这个缺陷的方式就是设置较短过期时间
          • 即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。

        QQ截图20220119210612

  • 缓存击穿

    缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中
    对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一
    个屏障上凿开了一个洞。
    当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访
    问数据库来查询最新数据,并且回写缓存,会导使数据库瞬间压力过大

    • 解决方案
      • 设置热点数据永不过期
        • 这样就不会出现热点数据过期的情况,但是当Redis内存空间满的时候也会清理部分数据,而且此种方案会占用空间,一旦热点数据多了起来,就会占用部分空间。
      • 加互斥锁(分布式锁)
        • 在访问key之前,采用SETNX(set if not exists)来设置另一个短期key来锁住当前key的访问,访问结束再删除该短期key。保证同时刻只有一个线程访问。这样对锁的要求就十分高。
  • 缓存雪崩

    大量的key设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。

    • 解决方案
      • redis高可用
        • redis集群。(异地多活)
      • 限流降级
        • 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
      • 数据预热
        • 在正式部署之前,先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。

参考链接

posted @ 2022-01-19 22:12  Faetbwac  阅读(30)  评论(1编辑  收藏  举报