1. 背景

1.1 redis 的 noSql 属性

  • redis 支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用;
  • redis 不仅支持简单的 key-value 类型的数据,同时还提供 list、set、hash 等数据结构的存储;
  • redis 支持数据的备份、集群等高可用功能,redis可支持 16 个数据库;

    1.2 特性与局限

  • 特性

    • 性能极高,能读的速度是 110000 次/秒,写的速度是 81000 次/秒;
    • 单个 key 可存入 512M 大小;
    • 丰富的数据类型,支持类型包括:String、List、Set 及 Ordered Set;
    • 原子性;
    • 丰富的特性:支持 publish/subscribe,通知,key 过期等特性,可以做消息队列,如聊天室、IM;
  • 局限性

    • 持久化
      • 定时快照(snapshot):每隔一段时间将整个数据库全部写到磁盘上,代价太高;
      • 基于语句追加(aof):只追踪变化的数据,但追加的 log 可能过大,同时所有的操作均重新执行一遍,恢复速度慢;
    • 耗内存,占用内存过高;

      2. 安装与基本配置

      3. redis 命令

      3.1 redis 使用指令

  • 在远程服务器上执行命令:redis-cli -h 127.0.0.1 -p 6379 -a 密码;

  • 启动 redis 服务器:redis-server redis.windows.conf
  • 启动 redis 客户端:redis-cli
  • redis 服务相关命令
    • 安装服务:redis-server —service-install redis.windows.conf
    • 卸载服务:redis-server —service-uninstall
    • 开启服务:redis-server —service-start
    • 停止服务:redis-server —service-stop
  • redis 关闭
    • 方式一:断电、非正常关闭,容易数据丢失;
      • ps -ef | grep -i redis
    • 方式二:正常关闭,数据保存,客户端 shutdown;

      3.2 通用操作命令

      ```
  1. del key
  2. dump key //序列化给定 key 并返回被序列化的值
  3. exists key
  4. expire key seconds(miliseconds) //为给定 key 设置过期时间
  5. pttl(key) //以毫秒(秒)为单位,返回给定 key 的剩余生存时间,若为 -1 表示永久有效,-2 表示无效
  6. persist key //移除 key 的过期时间,key 将持久保持
  7. keys pattern //查找所有符合给定模式的 key,pattern 通配符,* 表示所有,?表示一个字符
  8. random key //从当前数据库中随机返回一个 key
  9. rename key newkey //重命名 key 10.move key db//从当前数据库移动到给定的数据库 db 中 11.type key ```
  • 应用 expire key seconds
    • 限时优惠活动信息;
    • 网站数据缓存(对于一些需要定时更新的数据,如:积分排行榜);
    • 手机验证码;
    • 限制网站访客访问频率(如:一分钟最多访问 10 次);
  • key 命名建议:

    • key 不要过长(1024)或过短,建议统一命名(eg. user:name:cyt),key名称区分大小写;

      3.3 五种数据类型

      3.3.1 string

  • 应用场景

    • string 通常用于保存单个字符串或 json 字符串数据;
    • string 二进制安全,可把一个图片文件作为字符串存储;
    • 计数器:常规 key-value 缓存应用,如统计粉丝数; ``` 赋值
  1. set key_name value
  2. setnx key value //只有在 key 不存在时设置 value【解决分布式锁 方案之一】
  3. mset key value[key value …] //同时设置一个或多个 key-value 对 取值
  4. get key_name
  5. getrange key start end //类似于 subString
  6. getbit key offset //对 key 所存储的字符串值,获取指定偏移量上的位(bit)
  7. mget key1 [key2 ..]
  8. getset key_name value //设置指定 key 的值,并返回 key 的旧值,当 key 不存在时,返回 nil
  9. strlen key 删除
  10. del key_name 自增/自减
  11. incr key_name //将 key 增 1,若 key 不存在,则 key 会被初始化为 0,然后再执行此操作
  12. incrby key_name 增量值 //将 key 加上指定增量值
  13. decr key_name
  14. decrby key_name 减值 字符串拼接 append key_name value //为指定的 key 追加至末尾,若不存在,为其赋值 ```

    3.3.2 hash

  • 应用场景
    • hash 常用于存储一个对象,例如用户信息数据,key 为用户 ID,value 为用户对象的姓名、年龄、生日等信息,value 以序列化方式存储;
      • 缺点:增加序列化/反序列化开销,且在需要修改其中一项信息时,需要把整个对象取回,并且修改操作需对并发进行保护,引入 CAS 等复杂问题;
    • 为什么不用 string 存储一个对象
      • 用户 ID + 对应属性名称作为唯一标识,虽然省去序列化开销和并发问题,但用户 ID 重复存储,存在内存浪费 ``` 赋值
  1. hset key field value
  2. hmset key field value[field1 value1 …]
  3. hsetnx kkey field value //只有在字段 field 不存在时,设置 hash 表字段的值 取值
  4. hget key field
  5. hmget field[field1 ..]
  6. hgetall key //返回 hash 表中所有的字段和值
  7. hkeys key //获取所有 hash 表中的字段
  8. hlen key //获取 hash 表中字段的数量
  9. hexists key field //查看 hash 表 key 中,指定的字段是否存在 删除
  10. hdel key field1[field2..] //删除一个或多个 hash 表字段 自增/自减
  11. hincrby key field value
  12. hincrbyfloat key field increment ```

    3.3.3 list

  • 用来存储多个 有序字符串。在 Redis 中,可以对列表的 两端 进行 插入push)和 弹出pop)操作,还可以获取 指定范围元素列表、获取 指定索引下标元素 等;
  • 内部编码
    • ziplist(压缩列表):当元素 个数较少没有大元素,目的是 减少内存的使用;
    • linkedlist(链表): 当元素个数超过 512 个 或 某个元素超过 64 字节;
  • 应用场景
    • 阻塞式消息队列、文章列表(对数据量大的集合进行删减)等; | 命令组合 | 对应数据结构 | | :—- | :—- | | lpush + lpop | Stack(栈) | | lpush + rpop | Queue(队列) | | lpush + ltrim | Capped Collection(有限集合) | | lpush + brpop | Message Queue(消息队列) |

Redis - 图1

  1. 添加
  2. 1. rpush key value [value ...] //从右边插入元素
  3. 2. lpush key value [value ...]
  4. 3. linsert key before|after pivot value //从列表中找到第一个等于 pivot 的元素,在其前或者后插入一个新的元素value,eg:linsert listkey before b redis
  5. 查询
  6. 1. lrange key start stop //获取指定范围内的元素列表,与 python 用法一致,eg:lrange 0 -1 可从左到右获取列表所有元素;
  7. 2. lindex key index //获取 list 指定索引下标的元素
  8. 3. llen key //获取列表长度
  9. 删除
  10. 1. lpop key //从列表左侧弹出元素
  11. 2. rpop key
  12. 3. lrem key count value //从等于value的元素删除count个元素,count>0时删右,count<0时删左,count=0删除所有;
  13. 4. ltrim listkey start end //裁剪列表,获得 start 与 end 之间的
  14. 修改
  15. 1. lset key index newValue //修改指定索引下标的元素
  16. 阻塞
  17. 1)列表为空
  18. a. 如果timeout不为0,则客户端要等到timeout秒后返回,如果timeout=0,则客户端一直阻塞;
  19. b. 期间若有数据添加进去,客户端立即返回;
  20. 2)列表不为空
  21. a. 如果是多个键,那么brpop会从左至右遍历键,一旦有一个能弹出元素,客户端立即返回;
  22. b. 如果多个客户端对同一个键执行brpop,那么最先执行brpop命令的客户端可以获取到弹出的值;
  23. 1. blpop key [key ...] timeout
  24. 2. brpop key [key ...] timeout

Redis - 图2

3.3.4 set

  • 支持 集合内增删改查,同时还支持 多个集合交集并集差集;
  • 集合类型内部编码 有两种:
    • intset(整数集合):集合中的元素都是 整数元素个数 小于 set-max-intset-entries 配置(默认 512 个)时,Redis 会选用 intset 来作为 集合内部实现,从而 减少内存 的使用;
    • hashtable(哈希表):当集合类型 无法满足 intset 的条件时,Redis 会使用 hashtable 作为集合的 内部实现
  • 应用场景:标签功能,如电商人群分类,针对不同 tag 类型的人群做针对性推荐; ``` 集合内的操作命令
  1. sadd key element [element …] //添加元素
  2. srem key element [element …] //删除元素
  3. scard key //统计元素个数,时间复杂度为o(1)
  4. sismember key element //判断元素是否存在,是的话返回1,否则返回0
  5. srandmember key [count] //随机返回具有指定个数的元素,默认 count 为 1
  6. spop key //从集合随机弹出元素,且此元素会从集合中删除
  7. smembers key //获取集合中的所有元素,且返回结果无序

集合间的操作命令

  1. sinter key [key …] //求多个集合的交集
  2. suinon key [key …] //求多个集合的并集
  3. sdiff key [key …] //求多个集合的差集
  4. sinterstore destination key [key …] //保存交集结果
  5. suionstore destination key [key …] //保存并集结果
  6. sdiffstore destination key [key …] //保存差集结果 ``` Redis - 图3

    3.3.5 sorted set(zset)

  • 与 set 不同的是每个元素都会关联一个 double 类型的分数,redis 通过分数来为集合中的成员进行从小到大的排序;
  • 有序集合的成员是唯一的,但 score 可以重复;
  • 集合通过 hash 表实现,所以添加、删除、查找的复杂度都是 o(1);
  • 集合最大可存储 40 多亿个成员;
  • 应用场景:排行榜 ``` 赋值
  1. zadd key score1 member1 [score2 member2] //向有序集合添加一个或多个成员,或者更新已存在成员的分数

取值

  1. zcard key //获取有序集合的成员数
  2. zcount key min max //计算在有序集合中指定区间分数的成员数
  3. zrank key member //返回有序集合中指定成员的索引
  4. zrange key start stop[with scores] //通过索引区间返回有序集合中指定区间内的成员[低到高]
  5. zrevrange key start stop[with scores]

删除

  1. del key //移除集合
  2. zrem key member[member …]
  3. zremrangebyrank key start sop //移除有序集合中给定的排名区间的所有成员(低到高排序)
  4. zremrangebyscore key min max //移除有序集合中给定的分数区间的所有成员 ```

4. redis 主从复制

  • 作用

    • 为数据提供多个副本,实现高可用
    • 实现读写分离(主节点负责写数据,从节点负责读数据,主节点定期把数据同步到从节点保证数据的一致性)

      4.1 配置

  • 命令slaveof。
    优点:无需重启。缺点:不便于管理

    // 命令行使用
    slaveof ip port // 使用命令后自身数据会被清空,但取消slave只是停止复制,并不清空复制代码
    
  • 修改配置。
    优点:统一配置。缺点:需要重启

    // 配置文件中配置
    slaveof ip port
    slave-read-only yes //只允许从节点进行读操作复制代码
    

    4.2 方式

    4.2.1 全量复制

  • 概念

    • 用于初次复制或其它无法进行部分复制的情况,将主节点中的所有数据都发送给从节点,是一个非常重型的操作,当数据量较大时,会对主从节点和网络造成很大的开销

Redis - 图4

  • 过程
    • Redis内部会发出一个同步命令,刚开始是Psync命令,Psync ? -1表示要求master主机同步数据;
    • 主机会向从机发送run_id和offset,因为slave并没有对应的 offset,所以是全量复制;
    • 从机slave会保存主机master的基本信息;
    • 主节点收到全量复制的命令后,执行bgsave(异步执行),在后台生成RDB文件(快照),并使用一个缓冲区(称为复制缓冲区)记录从现在开始执行的所有写命令;
    • 主机发送RDB文件给从机;
    • 发送缓冲区数据;
    • 刷新旧的数据。从节点在载入主节点的数据之前要先将老数据清除;
    • 加载RDB文件将数据库状态更新至主节点执行bgsave时的数据库状态和缓冲区数据的加载。
  • 开销

    • 主节点需要bgsave
    • RDB文件网络传输占用网络io
    • 从节点要清空数据
    • 从节点加载RDB
    • 全量复制会触发从节点AOF重写

      4.2.2 部分复制

  • 概念

    • 用于部分复制是Redis 2.8以后出现的,用于处理在主从复制中因网络闪断等原因造成的数据丢失场景,当从节点再次连上主节点后,如果条件允许,主节点会补发丢失数据给从节点。因为补发的数据远远小于全量数据,可以有效避免全量复制的过高开销,需要注意的是,如果网络中断时间过长,造成主节点没有能够完整地保存中断期间执行的写命令,则无法进行部分复制,仍使用全量复制

Redis - 图5

  • 过程
    • 如果网络抖动(连接断开 connection lost);
    • 主机master 还是会写 repl_back_buffer(复制缓冲区);
    • 从机slave 会继续尝试连接主机;
    • 从机slave 会把自己当前 run_id 和偏移量传输给主机 master,并且执行 pysnc 命令同步;
    • 如果master发现你的偏移量是在缓冲区的范围内,就会返回 continue命令;
    • 同步了offset的部分数据,所以部分复制的基础就是偏移量 offset;

      4.2.3 常见问题及解决

  1. 读写分离
    • 复制数据存在延迟(如果从节点发生阻塞)
    • 从节点可能发生故障
  2. 主从配置不一致
    • 例如maxmemory不一致,可能会造成丢失数据
    • 例如数据结构优化参数不一致:造成主从内存不一致
  3. 规避全量复制

    • 第一次全量复制不可避免,所以分片的maxmemory减小,同时选择在低峰(夜间)时,做全量复制。
    • 复制积压缓冲区不足
      增大复制缓冲区配置rel_backlog_size

      例如如果网络中断的平均时间是60s,而主节点平均每秒产生的写命令(特定协议格式)所占的字节数为100KB,则复制积压缓冲区的平均需求为6MB,保险起见,可以设置为12MB,来保证绝大多数断线情况都可以使用部分复制。

  4. 复制风暴
    master节点重启,master节点生成一份rdb文件,但是要给所有从节点发送rdb文件。对cpu,内存,带宽都造成很大的压力

    5. redis 集群

    5.1 介绍

    概念
    优势
    局限

    5.2 分布式方案

    5.2.1 客户端方案

    客户端 就已经决定数据会被 存储 到哪个 redis 节点或者从哪个 redis 节点 读取数据。其主要思想是采用 哈希算法Redis 数据的 key 进行散列,通过 hash 函数,特定的 key映射 到特定的 Redis 节点上。
    Redis - 图6
    客户端分区方案 的代表为 Redis ShardingRedis ShardingRedis Cluster 出来之前,业界普遍使用的 Redis 多实例集群 方法。JavaRedis 客户端驱动库 Jedis,支持 Redis Sharding 功能,即 ShardedJedis 以及 结合缓存池ShardedJedisPool

    5.2.2 代理分区方案

    客户端 发送请求到一个 代理组件代理 解析 客户端 的数据,并将请求转发至正确的节点,最后将结果回复给客户端。

  • 优点:简化 客户端 的分布式逻辑,客户端 透明接入,切换成本低,代理的 转发存储 分离。
  • 缺点:多了一层 代理层,加重了 架构部署复杂度性能损耗

Redis - 图7
代理分区 主流实现的有方案有 TwemproxyCodis

5.2.3 查询路由方案

客户端随机地 请求任意一个 Redis 实例,然后由 Redis 将请求 转发正确Redis 节点。Redis Cluster 实现了一种 混合形式查询路由,但并不是 直接 将请求从一个 Redis 节点 转发 到另一个 Redis 节点,而是在 客户端 的帮助下直接 重定向redirected)到正确的 Redis 节点;
Redis - 图8

  • 优点
    • 无中心节点,数据按照 存储分布在多个 Redis 实例上,可以平滑的进行节点 扩容/缩容,支持 高可用自动故障转移,运维成本低。
  • 缺点

    • 严重依赖 Redis-trib 工具,缺乏 监控管理,需要依赖 Smart Client (维护连接缓存路由表MultiOpPipeline 支持)。Failover 节点的 检测过慢,不如 中心节点 ZooKeeper 及时。Gossip 消息具有一定开销。无法根据统计区分 冷热数据

      5.3 数据分区

      5.4 功能限制

  • key 批量操作 支持有限

    • 类似 msetmget 操作,目前只支持对具有相同 slot 值的 key 执行 批量操作。对于 映射为不同 slot 值的 key 由于执行 mgetmget 等操作可能存在于多个节点上,因此不被支持;
  • key 事务操作 支持有限
    • 只支持 key同一节点上事务操作,当多个 key 分布在 不同 的节点上时 无法 使用事务功能;
  • key 作为 数据分区 的最小粒度
    • 不能将一个 大的键值 对象如 hashlist 等映射到 不同的节点
  • 不支持 多数据库空间
    • 单机 下的 Redis 可以支持 16 个数据库(db0 ~ db15),集群模式 下只能使用 一个 数据库空间,即 db0
  • 复制结构 只支持一层

    • 从节点 只能复制 主节点,不支持 嵌套树状复制 结构;

      5.5 搭建【实践】

      6. Java 连接 redis 数据库【实战】

      6.1 RedisUtil 工具类

      6.2 SpringData 整合 redis

      7. 其他

      7.1 redis 发布与订阅功能

  • 订阅

    • subscribe channel [channel …] //订阅给定的一个或多个频道的信息
    • psubscribe pattern [pattern …] //订阅一个或多个符合给定模式的频道
  • 发布
    • publish channel message //将信息发送到指定的频道
  • 退订
    • unsubscribe [channel [channel …]]
    • punsubscribe [pattern [pattern …]]
  • 应用场景:构建实时消息系统,如微博、微信公众号等

    7.2 redis 多数据库

  • redis 下,数据库由一个整数索引标识,而不是一个数据库名称。默认情况下,一个客户端连接到数据库 0,范围 0~15 整数;

    select 2 //数据库切换
    move key 名称 数据库 //移动数据
    flushdb //清除当前数据库的所有 key
    flushall //清除所有 redis 数据库 key
    

    7.3 redis 事务

  • redis 事务可以一次执行多个命令;

  • redis 会将一个事务中的所有命令序列化,然后按顺序执行;
  • 执行中不会被其他命令插入,不许出现加塞行为;
  • 一个事务从开始到执行经历的三个阶段:开始事务、命令入队、执行事务;
  • 事务的错误处理
    • 【执行阶段】若执行的某个命令报出了错误(业务逻辑问题),则只有报错的命令不会被执行,而其它命令都会执行,不会回滚;
    • 【组队阶段】队列中的某个命令出现了报告错误执行时整个所有队列都会被取消;

clipboard.png

  • 应用场景:商品秒杀活动、转账活动等;

    discard //取消事务,放弃执行事务块内的所有命令
    exec //执行所有事务块内的命令
    multi //标记一个事务块的开始
    unwatch //取消 watch 命令对所有 key 的监视
    watch key[key ...] //监视一个或多个 key,若在事务执行之前这个 key 被其他命令更改则事务被打断
    

    7.4 redis 内存管理

  • 内存设置:建议内存空间不超过 1G 256-512M;

  • 为数据设置超时时间;
  • 采用 LRU 算法(页面置换算法)动态将不用的数据删除;

    7.5 redis 数据淘汰策略

  • 若无淘汰策略或找不到适合淘汰的 key 时,直接返回 out of memory 错误

  • 最大缓存配置: maxmemory 512G
  • 6 种数据淘汰策略

    • volatile-lru:从一设置过期时间的数据集中挑选最近最少使用的数据淘汰;
    • volatile-lfu:从已设置过期的 keys 中,删除一段时间内使用次数最少的;
    • volatile-ttl:从已设置过期时间的数据集中挑选最近将要过期的数据淘汰;
    • volatile-random:从已设置过期时间的数据集中随机选择数据淘汰;
    • allkeys-lru:从数据集中挑选最近最少使用的数据淘汰;
    • allkeys-lfu:从所以 keys 中删除一段时间内使用次数最少的;
    • allkeys-random:从数据集中随机选择数据淘汰;
    • no-enviction:默认配置,不采用任何淘汰策略;

      7.6 redis 持久化

  • RDB(快照)

    • 默认持久化方式,将内存中的数据已快照的方式写入二进制文件中,默认文件名:dump.rdb
    • 特点:在指定时间间隔后执行
    • 优点:保存和还原数据极快,适用于灾难备份
    • 缺点:不适合小内存机器,RDB 机制符合要求就会照快照
    • 快照条件
      • 服务器正常关闭时: redis-cli shutdown
      • key 满足一定条件,会进行快照,如 save 900 1 //每 900 秒至少 1 个 key 发生变化,产生快照
  • AOF

    • 适用于不能产生任何修改丢失的场景
    • 实现:将收到的每一个写命令通过 write 函数追加到文件中(默认为 appendonly.aof),当 redis 重启时会通过重新执行文件中保存的写命令在内存中重建整个数据库的内容
    • 3 种方式(默认:每秒 fsync 一次)
      • appendonly yes //启用 aof 持久化方式
      • appendfsync always //收到写命令就立即写入磁盘,最慢,但保证完全的持久化
      • appendfsync everysec //每秒钟写入磁盘一次,在性能和持久化方面做了很好的折中
      • appendfsync no //完全依赖 os,性能最好,持久化没保证
    • 缺点:冗余命令导致持久化文件过大

      7.7 redis 缓存

      缓存穿透
  • 缓存穿透

    • 查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义,在流量大时,可能导致 DB 挂掉;
    • 解决
      • 布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被 这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力;
      • 如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),对空结果进行限时缓存,设置很短的过期时间;
  • 缓存击穿
    • 缓存在某个时间点过期时,恰好在此节点对这个 Key 有大量并发请求过来,这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端 DB 压垮;
  • 缓存雪崩

    • 设置缓存时采用相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到 DB,DB 瞬时压力过重造成雪崩;
    • 解决
      • 用加锁或者队列的方式保证缓存的单线 程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上;
      • 将缓存失效时间分散开,降低缓存失效时间的重复率;

        7.7.2 保持与数据库的一致性

  • 实时同步

    • 适用:对强一致性要求较高
    • 实现:查询时缓存有限,若无则经数据库查询后保存到缓存;更新缓存时先更新数据库,再将缓存设置过期
    • 使用
      • @Cacheable:查询时使用,注意 Long 类型需转换为 String 类型,否则会抛出异常
      • @CachePut:更新时使用,使用此注解,一定会从 DB 上查询数据
      • @CacheEvict:删除时使用
      • @Caching:组合用法
  • 异步队列
  • 阿里工具 canal
  • 采用 UDF 自定义函数的方式

    参考

    深入剖析Redis系列(三) - Redis集群模式搭建与原理详解
    缓存穿透,缓存击穿,缓存雪崩解决方案分析
    Redis主从复制的原理