个人博客

欢迎访问个人博客: https://www.crystalblog.xyz/

备用地址: https://wang-qz.gitee.io/crystal-blog/

1. Redis入门

1.1 Redis简介

B站视频: Redis入门到精通,深入剖析Redis缓存技术,Java企业级解决方案必看的redis教程

Redis官网

Redis中文网

https://redis.com.cn/

http://www.redis.cn/

https://www.redis.net.cn/

1.1.1 Redis引入

2007年10月30日, 北京奥运会门票面向公众第二阶段预售启动. 上午官方网站1h内的浏览量达到了800万次, 票务中心呼入量超过了380万人次. 由于瞬间访问数量过大, 技术系统应对不畅, 造成很多申购者无法及时提交申请.

问题现象

  • 海量用户
  • 高并发

罪魁祸首-关系型数据库

  • 性能瓶颈: 磁盘IO性能低下
  • 扩展瓶颈: 数据关系复杂, 扩展性差, 不便于大规模集群

解决思路

  • 降低磁盘IO次数, 越低越好 . — 内存存储
  • 去除数据间关系, 越简单越好 . — 不存储关系, 仅存储数据

1.1.2 NoSQL

NoSQL, 即not-only SQL(泛指非关系型数据库), 作为关系型数据库的补充.

作用: 应对基于海量用户和海量数据前提下的数据处理问题.

特征:

  • 可扩容, 可伸缩
  • 大数据量下高性能
  • 灵活的数据模型
  • 高可用

常见NoSQL数据库

  • Redis
  • memcache
  • HBase
  • MongoDB

解决方案(电商场景)
在这里插入图片描述

1.1.3 Redis概念

Redis(Remote Dictionary Server), 是用C语言开发的一个开源的高性能键值对(key-value)数据库.

特征:

  • 数据间没有必然的关联关系.
  • 内部采用单线程机制进行工作
  • 高性能. 官方提供测试数据, 50个并发执行10万个请求, 读的速度是11万次/s, 写的速度是8.1万次/s.
  • 多数据类型支持(字符串string, 列表list, 集合set, 有序集合sorted Set)
  • 持久化支持. 可以进行数据灾难恢复.

1.1.4 Redis应用

  • 为热点数据加速查询, 如热点商品, 热点新闻, 热点资讯等高访问量信息等;
  • 任务队列, 如秒杀, 抢购, 购票排队等;
  • 即时信息查询, 如排行榜, 网站访问统计, 在线人数(聊天室)等;
  • 时效性信息控制, 如验证码, 股票控制等;
  • 分布式数据共享, 如分布式集群架构中的session分离
  • 消息队列
  • 分布式锁

1.2 Redis下载与安装

Linux版本(适用于企业级开发)

Windows版本(适合零基础学习)

1.3 Redis的基本操作

添加: set key value

获取: get key

帮助: help 命令, 例如 help get
在这里插入图片描述

退出客户端: quit/exit/<ESC>

2. 数据类型

2.1 数据使用场景

业务数据的特殊性, 作为缓存使用

  • 原始业务功能设计 (秒杀, 618活动, 双11活动, 12360排队购票)
  • 运营平台监控到的突发高频访问数据 (突发时政要闻)
  • 高频, 复杂的统计数据 (在线人数)

附加功能, 系统功能优化或升级

  • 单机服务升级集群
  • session管理
  • token管理

2.2 数据存储格式

redis自身是一个Map, 其中所有的数据都是采用key:value的形式存储, 最大存储512M数据.
在这里插入图片描述

数据类型指的是存储的数据的类型, 也就是value部分的类型, key部分永远都是字符串类型.

2.3 string

存储的数据: 单个数据, 最简单的数据存储类型, 也是最常用的数据存储类型.

存储数据的格式: 一个存储空间保存一个数据.

存储的内容: 通常使用字符串, 如果字符串以整数的形式展示, 可以作为数字操作使用.

2.3.1 基本操作

添加/修改数据: set key value

获取数据: get key

删除数据: del key

添加/修改多个数据: mset key1 value1 key2 value2...

获取多个数据: mget key1 key2 ...

获取数据字符个数(字符串长度) : strlen key

追加信息到原始信息后面: append key value (如果key存在就追加, 不存在则创建)

设置数据指定的生命周期: setex key seconds value , psetex key millseconds value

查看指定key的剩余有效期: ttl key

2.3.2 业务场景1


大型企业级应用中, 分表操作是基本操作, 使用多张表存储同类型数据, 但是对应的主键id必须保证统一性, 不能重复. oracle数据库具有sequence设定, 可以解决该问题, 但是MySql数据库并没有类似的机制, 如何解决.

解决方案

  • 设置数值数据增加指定范围的值

    incr key

    incrby key increment

    incrbyfloat key increment

  • 设置数值数据减少指定范围的值

    decr key

    decrby key decrement

string 作为数值操作

  • string在redis内部存储默认就是一个字符串, 当遇到增减操作incr, decr时会转换成数值进行计算.
  • redis所有的操作都是原子性的, 采用单线程处理所有业务, 命令是一个一个执行的, 因此无需考虑并发带来的数据影响.
  • 注意, 按数值进行操作的数据, 如果原始数据不能转换成数值, 或超越了redis数值的上限范围, 将会报错. (最大值, Long类型的上限值. Long.MAX_VALUE)

Tips

  • redis用于控制数据库表主键id, 为数据库表主键提供生成策略, 保障数据库表的主键唯一性.
  • 此方案适用于所有数据库, 且支持数据库集群.

2.3.3 业务场景2

主页高频访问信息显示控制, 例如新浪微博大V主页显示关注数, 粉丝数与微博数量.
在这里插入图片描述

解决方案

(1) 在redis中为大V用户设定用户信息, 以用户主键和属性值作为key, 后台设定定时刷新策略即可.

关注数量: user:id:3506728370:focus >> 83

粉丝数量: user:id:3506728370:fans >> 12210947

微博数量: user:id:3506728370:blogs >> 6164

(2) 在redis中以json格式存储大V用户信息, 定时刷新.

user:id:3506728370 {id:3506728370 ,fans:12210947 ,blogs: 6164, focus: 83}

Tips

redis应用于各种结构型和非结构型高热度数据访问加速.

数据库中的热点数据key命名惯例: 表名:主键名:主键值:字段属性名

2.4 hash

对象数据的存储如果发生比较频繁的更新操作, 如果使用上面的表名:主键名:主键值:字段属性名存string类型会显得比较笨重. 可以使用hash存储.

hash类型, 底层使用哈希表结构实现数据存储. 结构如下:
在这里插入图片描述

2.4.1 基本操作

添加/修改数据: hset key field value

获取数据: hget key field , hgetall key

删除数据: hdel key field [field2]...

添加/修改多个数据: hmset key field1 value1 field2 value2...

获取多个数据: hmget key field1 field2...

获取哈希表字段的数量: hlen key

获取哈希表中是否存在指定的字段: hexists key field

获取哈希表中所有字段名或字段名: hkeys key , hvals key

设置指定字段的数值数据增加指定范围的值: hincrby key field increment , hincrbyfloat key field increment

2.4.2 注意事项

  • hash类型的value只能存储字符串, 不允许存储其他数据类型, 不存在嵌套. 如果数据没有获取到, 对应的值为nil.
  • 每个hash可以存储(2^32-1)个键值对.
  • hash类型十分贴近对象的数据存储形式, 并且可以灵活添加删除对象属性, 但hash设计初衷不是为了存储大量对象而设计的, 切记不可滥用, 更不可以将hash作为对象列表使用.
  • hgetall操作可以获取全部属性, 如果内部field字段过多, 遍历整体数据效率就会很低, 有可能造成数据访问瓶颈.

2.4.3 业务场景1

redis应用于电商网站购物车设计实现.
在这里插入图片描述

以上仅仅是将数据存储到了redis中, 并没有起到加速的作用, 商品信息还需要二次查询数据库.
在这里插入图片描述

# 003 表示购物车
127.0.0.1:6379> hmset 003 g01:nums 100 g01:info {...}
OK
127.0.0.1:6379> hgetall 003
1) "g01:nums"
2) "100"
3) "g01:info"
4) "{...}"
127.0.0.1:6379> hmset 003 g02:nums 200 g02:info {...}
OK
127.0.0.1:6379> hgetall 003
1) "g01:nums"
2) "200"
3) "g01:info"
4) "{...}"
5) "g02:nums"
6) "200"
7) "g02:info"
8) "{...}"
127.0.0.1:6379>

2.4.4 业务场景2

双11活动日, 销售手机充值卡的商家对移动, 联通, 电信的30元, 50元, 100元商品推出抢购活动, 每种商品抢购上限1000张.

解决方案:

  • 以商家id作为key
  • 将参与抢购的商品id作为field
  • 将参与抢购的商品数量作为对应的value
  • 抢购时使用降值的方式控制产品数量
  • 实际业务中还有超卖等实际问题, 后续讨论.

p01表示商家ID, c30, c50, c100表示商品ID

127.0.0.1:6379> hmset p01 c30 1000 c50 1000 c100 1000
OK
127.0.0.1:6379> hgetall p01
1) "c30"
2) "1000"
3) "c50"
4) "1000"
5) "c100"
6) "1000"
127.0.0.1:6379> hincrby p01 c50 -1
(integer) 999
127.0.0.1:6379> hincrby p01 c100 -20
(integer) 980
127.0.0.1:6379> hgetall p01
1) "c30"
2) "1000"
3) "c50"
4) "999"
5) "c100"
6) "980"
127.0.0.1:6379>

2.5 list

存储多个数据, 并对数据进行存储空间的顺序进行区分;

一个存储空间保存多个数据, 且通过数据可以体现进入属性;

list可以保存多个数据, 底层使用双向链表存储结构实现.
在这里插入图片描述

2.5.1 基本操作

应用于具有操作先后顺序的数据控制.

添加/修改操作: lpush key value1 [value2]... , rpush key value1 [value2]...

获取数据: lrange key start stop , lindex key index , llen key

获取并移除数据: lpop key , rpop key

规定时间内获取并移除数据 (阻塞): blpop key1 [key2] timeout , brpop key1 [key2] timeout

移除指定数据: lrem key count value

2.5.2 注意事项

  • list中保存的数据都是string类型的, 数据总容量是有限的, 最多(2^32-1)个元素.
  • list具有索引的概念, 但是操作数据时通常以队列的形式进行入队出队操作, 或以栈的形式入栈出栈操作.
  • 获取全部数据操作结束索引设置为 -1 . 比如: lrange key 0 -1
  • list可以对数据进行分页操作, 通常第一页的信息来自于list, 第二页及更多的信息通过数据库的形式加载.

2.5.3 业务场景1

微信朋友圈点赞, 要求按照点赞顺序显示点赞好友信息.

如果取消点赞, 移除对应好友信息. (lrem )
在这里插入图片描述

2.5.4 业务场景2

  • twitter, 新浪微博, 腾讯微博中个人用户的关注列表需要按照用户的关注顺序进行展示, 粉丝列表需要将最近关注的粉丝列在最前面.
  • 新闻, 资讯类网站如何将最新的新闻或资讯按照发生的时间顺序展示?
  • 企业运营过程中, 系统将产生出大量的运营数据, 如何保障多台服务器操作日志的统一顺序输出?

解决方案:

  • 依赖list的数据具有顺序的特征对信息进行管理
  • 使用队列模型解决多路信息汇总合并的问题
  • 使用栈模型解决最新消息的问题

2.6 set

能够存储大量的数据, 高效的内部存储机制, 提供更高的查询效率.

与hash存储结构完全相同, 仅存储键, 不存储值(nil), 并且不允许键重复.
在这里插入图片描述

2.6.1 基本操作

添加数据: sadd key member1 [member2]

获取全部数据: smembers key

删除数据: srem key member1 [member2]

获取集合长度: scard key

判断集合中是否包含指定数据: sismember key member

随机获取集合中指定数量的数据: srandmember key [count]

随机获取集合中的某个数据并将该数据移除集合: spop key

求两个集合的交,并,差集:
sinter key1 [key2]
sunion key1 [key2]
sdiff key1 [key2]

求两个集合的交,并,差集并存储到指定集合中:
sinterstore destination key1 [key2]
sunionstore destination key1 [key2]
sdiffstore destination key1 [key2]

将指定数据从原始集合中移动到目标集合中: smove source destination member

2.6.2 注意事项

  • set类型不允许数据重复, 如果添加的数据在set中已经存在,将只保留一份;
  • set虽然与hash的存储结构相同, 但是无法启用hash中存储值的空间.

2.6.3 业务场景1

每位用户首次使用今日头条时会设置3项爱好的内容, 但是后期为了增加用户的活跃度, 兴趣点, 必须让用户对其他信息类别逐渐产生兴趣, 增加用户留存度, 如何实现?

业务分析:

  • 系统分析出各个分类的最新或最热点信息条目并组织成set集合
  • 随机挑选其中部分信息( srandmember , spop )
  • 配合用户关注信息分类中的热点信息组织成展示的全部信息集合

Tips:

redis应用于随机推荐类信息检索, 例如热点歌单推荐, 热点新闻推荐, 应用app推荐, 大v推荐等.

2.6.4 业务场景2

在这里插入图片描述

分析:

使用set的获取交集, 并集, 差集的方案实现.

Tips:

  • redis应用于同类信息的关联搜索, 二度关联搜索, 深度关联搜索
  • 显示共同关注(一度)
  • 显示共同好友(一度)
  • 由用户A出发, 获取到好友用户B的好友信息列表(一度)
  • 由用户A出发, 获取到好友用户B的购物清单列表(二度)
  • 由用户A出发, 获取到好友用户B的游戏充值列表(二度)

2.6.5 业务场景3

集团公司共有12000名员工, 内部OA系统中具有700多个角色, 3000多个业务操作, 23000多种数据, 每位员工具有一个或多个角色, 如何快速进行业务操作的权限校验?
在这里插入图片描述

解决方案:

  • 依赖set集合数据不重复的特征, 依赖set集合hash存储结构特征完成数据过滤与快速查询
  • 根据用户id获取用户所有角色
  • 根据用户所有角色获取用户所有操作权限放入set集合
  • 根据用户所有角色获取用户所有数据全部放入set集合
127.0.0.1:6379> sadd rid:001 getall
(integer) 1
127.0.0.1:6379> sadd rid:001 getById
(integer) 1
127.0.0.1:6379> sadd rid:002 getall
(integer) 1
127.0.0.1:6379> sadd rid:002 insert
(integer) 1
127.0.0.1:6379> sunionstore uid:007 rid:001 rid:002
(integer) 3
127.0.0.1:6379> smembers uid:007  # redis提供基础数据
1) "insert"
2) "getById"
3) "getall"
127.0.0.1:6379> sismember uid:007 insert  # redis提供校验结果
(integer) 1

校验工作: redis提供基础数据还是提供校验结果? 推荐使用redis提供基础数据, 因为校验使用redis会造成业务与redis耦合, 侵入性增强了.

Tips:

redis应用于同类型不重复数据的合并操作

2.6.6 业务场景4

公司对旗下新的网站做推广, 统计网站的PV(访问量), UV(独立访客), IP(独立IP).

PV: 网站被访问次数, 可通过刷新页面提高访问量.

UV: 网站被不同用户访问的次数, 可通过cookie统计访问量, 相同用户切换IP地址, UV不变.

IP: 网站被不同IP地址访问的总次数, 可通过IP地址统计访问量, 相同IP不同用户访问, IP不变.

解决方案:

  • 利用set集合的数据不重复特征, 记录各种访问数据.
  • 建立string类型数据, 利用incr统计日访问量(PV)
  • 建立set模型, 记录不同cookie数量(UV)
  • 建立set模型, 记录不同IP数量(IP)
127.0.0.1:6379> sadd ips 1.2.3.4
(integer) 1
127.0.0.1:6379> sadd ips 2.3.4.5
(integer) 1
127.0.0.1:6379> sadd ips 2.3.4.5
(integer) 0
127.0.0.1:6379> scard ips
(integer) 2

Tips:

redis应用于同类型数据的快速去重, 进行数据过滤.

2.6.7 业务场景5

系统黑名单, 白名单.

解决方案:

  • 基于经营战略设定问题用户发现, 鉴别规则
  • 周期性更新满足规则的用户黑名单, 加入set集合
  • 用户行为信息到达后与黑名单进行比对, 确认行为去向
  • 黑名单过滤IP地址, 应用于开放游客访问权限的信息源
  • 黑名单过滤设备信息, 应用于限定访问设备的信息源
  • 黑名单过滤用户, 应用于基于访问权限的信息源

Tips:

redis应用于基于黑名单与白名单设定的服务控制.

2.7 sorted set

数据排序有利于数据的有效展示, 需要提供一种可以根据自身特征进行排序的方式.

新的存储模型, 在set的存储结构基础上添加可排序字段, 用来保存可排序的数据.

score不是真正的数据, 而是辅助排序的定义, 真正的业务数据在key中.
在这里插入图片描述

2.7.1 基本操作

添加数据: zadd key score1 member1 [score2 member2]

获取全部数据:
zrange key start stop [withscore]
zrevrange key start stop [withscore]

删除数据: zrem key member [member2...]

127.0.0.1:6379> zadd scores 94 zhangsan 100 lisi 60 wangwu 47 zhaoliu
(integer) 4
127.0.0.1:6379> zrange scores 0 -1
1) "zhaoliu"
2) "wangwu"
3) "zhangsan"
4) "lisi"
127.0.0.1:6379> zrange scores 0 -1 withscores
1) "zhaoliu"
2) "47"
3) "wangwu"
4) "60"
5) "zhangsan"
6) "94"
7) "lisi"
8) "100"

按条件获取数据:
zrangebyscore key min max [withscore] [limit]
zrevrangebyscore key max min [withscores]

按条件删除数据:
zremrangebyrank key start stop
zremrangebyscore key min max

127.0.0.1:6379> zrangebyscore scores 50 99  limit 0 3 withscores
1) "wangwu"
2) "60"
3) "zhangsan"
4) "94"
127.0.0.1:6379> zremrangebyrank scores 0 1
(integer) 2
127.0.0.1:6379> zrange scores 0 -1 withscores
1) "zhangsan"
2) "94"
3) "lisi"
4) "100"

注意:

  • min与max用于限定搜索查询的条件
  • start与stop用于限定查询范围, 作用于索引, 表示开始和结束索引
  • offset与count用于限定查询范围, 作用于查询结果, 表示开始位置和数据总量

获取集合数据总量:
zcard key
zcount key min max

集合交, 并集操作:
zinterstore destination numkeys key [key...]
zunionstore destination numkeys key [key...]

127.0.0.1:6379> zadd s1 50 aa 60 bb 70 cc
(integer) 3
127.0.0.1:6379> zadd s2 60 aa 40 bb 90 dd
(integer) 3
127.0.0.1:6379> zadd s3 70 aa 20 bb 100 dd
(integer) 3
127.0.0.1:6379> zinterstore ss 3 s1 s2 s3
(integer) 2
127.0.0.1:6379> zrange ss 0 -1 withscores ## 求交集后, 重复的member分数会求和
1) "bb"
2) "120"
3) "aa"
4) "180"

2.7.2 注意事项

  • score保存的数据存储空间是64位.

  • score保存的数据也可以是一个双精度的double值, 基于双精度浮点数的特征, 可能会丢失精度, 使用时要慎重.

  • zset底层存储还是基于set结构的, 因此数据不能重复, 如果重复添加相同的数据, score值将被反复覆盖, 保留最后一次修改的结果

    127.0.0.1:6379> zrange test 0 -1 withscores
    1) "aa"
    2) "11"
    127.0.0.1:6379> zadd test 22 aa
    (integer) 0
    127.0.0.1:6379> zrange test 0 -1 withscores
    1) "aa"
    2) "22"
    

2.7.3 业务场景1

票选广东十大杰出青年, 各类综艺选秀投票.

各类资源网站TOP10(电影, 歌曲,文档, 游戏等)

聊天室活跃度统计.

游戏好友亲密度.

分析:

  • 为所有参与排名的资源建立排序依据

解决方案:

获取数据对应的索引(排名):
zrank key member
zrevrank key member

score值获取与修改:
zscore key member
zincrby key increment member

127.0.0.1:6379> zadd movies 143 aa 97 bb 201 cc
(integer) 3
127.0.0.1:6379> zrank movies bb
(integer) 0
127.0.0.1:6379> zrevrank movies bb
(integer) 2
127.0.0.1:6379> zscore movies bb
"97"

Tips:

redis应用于计数器组合排序功能对应的排名

2.7.4 业务场景2

基础服务 + 增值服务类网站会设定各位会员的试用 , 让用户充分体验会员优势. 例如观影试用VIP, 游戏VIP体验, 云盘下载体验VIP, 数据查看体验VIP. 当VIP体验到期后, 如果有效管理此类信息, 即便对于正式VIP用户也存在对应的管理方式.

网站会定期开启投票, 讨论, 限时进行, 预期作废. 如何有效管理此类过期信息.

解决方案:

  • 对于基于时间线限定的任务处理, 将处理时间记录位score值, 利用排序功能区分处理的先后顺序.

  • 记录下一个要处理的时间, 当到期后处理对应的任务, 移除redis中的记录, 并记录下一个要处理的时间.

  • 当新任务加入时, 判定并更新当前下一个要处理的任务时间

  • 为提升zset的性能, 通常将任务根据特征存储成若干个zset, 例如1小时内, 1天内, 周内, 月内, 季内, 年度等, 操作时逐级提升, 将即将操作的若干个任务纳入到1小时内处理的队列中.

  • 获取当前系统时间 time

127.0.0.1:6379> zadd ts 1509802345 uid:001
(integer) 1
127.0.0.1:6379> zadd ts 1509802390 uid:007
(integer) 1
127.0.0.1:6379> zadd ts 1509802365 uid:088
(integer) 1
127.0.0.1:6379> zrange ts 0 -1 withscores
1) "uid:001"
2) "1509802345"
3) "uid:088"
4) "1509802365"
5) "uid:007"
6) "1509802390"
127.0.0.1:6379> time
1) "1646835417"
2) "261374"

Tips:

redis应用于定时任务执行顺序管理或任务过期管理.

2.7.5 业务场景3

任务/消息权重设定应用.

当任务或者消息待处理, 形成了任务队列或消息队列时, 对于高优先级的任务要保障对其优先处理, 如何实现任务权重管理呢?

解决方案:

  • 对于带有权重的任务, 优先处理权重高的任务, 采用score记录权重即可.

    多条件任务权重设定. 如果权重条件过多时, 需要对排序score值进行处理, 保障score值能够兼容2条件或多条件, 例如外贸订单优先于国内订单, 总裁订单优先于员工订单, 经理订单优先于员工订单.

  • 因score长度受限, 需要对数据进行截断处理, 尤其是时间设置为小时或分钟级即可.(折算后)

  • 先设定订单类别, 后设定订单发起角色类别, 整体score长度必须是统一的, 不足位补0. 第一排序规则首位不得是0.

    • 例如外贸101, 国内102, 经理004, 员工008
    • 员工下的外贸单score值为101008(优先)
    • 经理下的国内单score值为102004
127.0.0.1:6379> zadd tt 102004 order:id:1
(integer) 1
127.0.0.1:6379> zadd tt 101008 order:id:2
(integer) 1
127.0.0.1:6379> zrange tt 0 -1 withscores
1) "order:id:2"
2) "101008"
3) "order:id:1"
4) "102004"

Tips:

redis应用于即时任务/消息队列执行管理.

2.8 实践案例

2.8.1 案例1

人工智能领域的语义识别与自动对话将是未来服务业机器人应答呼叫体系中的重要技术, 百度自研用户评价语义识别服务, 免费开放企业试用, 同时训练百度自己的模型, 现对试用用户的使用行为进行限速, 限制每个用户每分钟最多发起10次调用.

解决方案:

  • 设计计数器, 记录调用次数, 用于控制业务执行次数, 以用户ID作为key, 使用次数作为value.
  • 在调用前获取次数, 判断是否超过限定次数
    • 不超过次数的情况下, 每次调用计数 +1
    • 业务调用失败, 计数 -1
  • 为计数器设置生命周期为指定周期, 例如 1秒/分钟, 自动清空周期内使用次数.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8x1zqxTO-1647738351020)(C:\Users\18482\AppData\Roaming\Typora\typora-user-images\image-20220310214111500.png)]

127.0.0.1:6379> get 415 # 415为用户ID
(nil)
127.0.0.1:6379> setex 415 60 1
OK
127.0.0.1:6379> get 415
"1"
127.0.0.1:6379> incr 415
(integer) 2
127.0.0.1:6379> get 415
"2"
127.0.0.1:6379> incr 415
(integer) 3

方案改进:

  • 取消最大值的判断, 利用incr操作超过最大值抛出异常的形式替代每次判断是否大于最大值
  • 判断是否为nil
    • 如果是, 设置Max-次数
    • 如果不是, 计数+1
    • 业务调用失败, 计数-1
  • 遇到异常, 即操作(+)超过上限, 视为使用达到上限.

在这里插入图片描述
操作实现

127.0.0.1:6379> get 415
(nil)
127.0.0.1:6379> setex 415 60 9223372036854775797 # 设置上限-10
OK
127.0.0.1:6379> get 415
"9223372036854775797"
127.0.0.1:6379> incr 415
(integer) 9223372036854775798
127.0.0.1:6379> incr 415
(integer) 9223372036854775799
127.0.0.1:6379> incr 415
(integer) 9223372036854775800
127.0.0.1:6379> incr 415
(integer) 9223372036854775801
127.0.0.1:6379> incr 415
(integer) 9223372036854775802
127.0.0.1:6379> incr 415
(integer) 9223372036854775803
127.0.0.1:6379> incr 415
(integer) 9223372036854775804
127.0.0.1:6379> incr 415
(integer) 9223372036854775805
127.0.0.1:6379> incr 415
(integer) 9223372036854775806
127.0.0.1:6379> incr 415
(integer) 9223372036854775807
127.0.0.1:6379> incr 415
(error) ERR increment or decrement would overflow # 10次后, 超过上限, 抛出异常

2.8.2 案例2

使用微信的过程中, 当微信接收消息后, 会默认将最近接收的消息置顶, 当多个好友及关注的订阅号同时发送消息时, 该排序会不停的进行交替, 同时还可以将重要的会话设置为置顶. 一旦用户离线后, 再次打开微信时, 消息该按照什么顺序展示呢?

业务分析
在这里插入图片描述

解决方案

  • 依赖list的数据具有顺序的特征对消息进行管理, 将list结构作为栈使用.
  • 对置顶与普通会话分别创建独立的 list 分别管理
  • 当某个 list 中接收到用户消息后, 将消息发送方的ID从 list 的一侧加入(此处设定在左侧)
  • 多个相同ID发出的消息反复入栈会出现问题, 在入栈前无论是否具有当前ID对应的消息, 先删除对应ID.
  • 推送消息时先推送置顶会话 list, 再推送普通会话 list, 推送完成的 list 清除所有数据.
  • 消息的数量, 也就是微信用户对话数量采用计数器的思想另行记录, 伴随 list 操作同步更新.
127.0.0.1:6379> lrem id:100 1 200
(integer) 0
127.0.0.1:6379> lpush id:100 200
(integer) 1
127.0.0.1:6379> lrem id:100 1 300
(integer) 0
127.0.0.1:6379> lpush id:100 300
(integer) 2
127.0.0.1:6379> lrem id:100 1 400
(integer) 0
127.0.0.1:6379> lpush id:100 400
(integer) 3
127.0.0.1:6379> lrem id:100 1 200
(integer) 1
127.0.0.1:6379> lpush id:100 200
(integer) 3
127.0.0.1:6379> lrem id:100 1 300
(integer) 1
127.0.0.1:6379> lpush id:100 300
(integer) 3
127.0.0.1:6379> lrange id:100 0 -1 # 最后消息展示顺序, 400 200 300
1) "300"
2) "200"
3) "400"

2.9 key通用操作

基本操作

删除指定key : del key

获取key是否存在: exists key

获取key的类型: type key

查询key : keys pattern ( * 匹配任意数量的任意符号, ? 匹配一个任意符号 []匹配一个指定符号)

为key改名: rename key newkey , renamenx key newkey

对所有key排序(list列表): sort [desc]

其他key通用操作: help @generic

扩展操作(时效性控制):

为指定key设置有效期

expire key seconds
pexpire key millseconds
expireat key timestamp
pexpireat key millseconds-timestamp

获取key的有效时间

ttl key
pttl key

切换key从时效性转换为永久性

persist key

2.10 数据库通用操作

在这里插入图片描述

db基本操作指令

切换数据库: select index

127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> select 15
OK

其他操作

quit
ping
echo message

数据移动 : move key db

127.0.0.1:6379> set name crysw
OK
127.0.0.1:6379> get name
"crysw"
127.0.0.1:6379> move name 1  # 如果1库有name, 会移动失败
(integer) 1
127.0.0.1:6379> get name
(nil)
127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> get name
"crysw"
127.0.0.1:6379[1]> select 0
OK
127.0.0.1:6379> set name panda
OK
127.0.0.1:6379> move name 1 # 返回0,移动失败
(integer) 0 

数据清除(慎用)

dbsize # 当前db中的key数量
flushdb
flushall

3. Jedis

3.1 Jedis简介

操作redis的java客户端, Jedis的api使用和redis的操作指令完全一样.

<dependency>
	<groupId>redis.clients</groupId>
	<artifactId>jedis</artifactId>
</dependency>

3.2 Jedis简单测试

@Test
public void testJedis() {
	// 1.连接redis
	Jedis jedis = new Jedis(host, port);
	System.out.println(jedis);
	jedis.auth(password);
	// 2. 操作redis
	jedis.set("name", " crysw");
	System.out.println("name=" + jedis.get("name"));
	System.out.println("ping=" + jedis.ping());
	// 关闭连接
	jedis.close();
}

3.3 Jedis简易工具类开发

JedisConfig配置类

@Configuration
@Slf4j
public class JedisConfig {

    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private int port;
    @Value("${spring.redis.password}")
    private String password;
    @Value("${spring.redis.timeout}")
    private int timeout;
    @Value("${spring.redis.jedis.pool.max-active}")
    private int maxActive;
    @Value("${spring.redis.jedis.pool.max-idle}")
    private int maxIdle;
    @Value("${spring.redis.jedis.pool.min-idle}")
    private int minIdle;
    @Value("${spring.redis.jedis.pool.max-wait}")
    private int maxWaitMillSeconds;

    @Bean
    public JedisPool jedisPool(){
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxIdle(maxIdle);
        jedisPoolConfig.setMinIdle(minIdle);
        jedisPoolConfig.setMaxTotal(maxActive);
        jedisPoolConfig.setMaxWaitMillis(maxWaitMillSeconds);

        JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, password);

        log.info("JedisPool连接成功: {}:{}",host,port);

        return jedisPool;
    }
}

Jedis工具类

/**
 * 类描述:Jedis工具类
 * @author crys
 * @date 2022/1/20 21:48
 * @version 1.0
 */
@Component
public class JedisUtils {
    @Autowired
    private JedisPool jedisPool;

    @Value("${spring.redis.password}")
    private String password;

    /**
     * 获取Jedis资源
     * @return
     */
    public Jedis getJedis(){
        Jedis jedis = jedisPool.getResource();
        jedis.auth(password);
        return jedis;
    }

    /**
     * 关闭jedis连接
     * @param jedis
     */
    public void close(Jedis jedis){
        if (jedis!=null) jedis.close();
    }

    // 为什么要封装工具类? Redis有很多指令, Jedis操作它需要针对业务场景做方法封装
    public int calcTimeHour(int hours){
        int seconds = hours*60*60;
        return  seconds;
    }
}

3.4 可视化客户端

远程连接, 使用RedisDesktopManager, 目前已经收费.

免费的redis可视化软件下载

推荐一款新的免费客户端: https://gitee.com/qishibo/AnotherRedisDesktopManager

默认不允许远程连接, 需要修改信息才允许进行连接.

# 注释掉, 允许本机以外的其他机器访问redis服务
#bind 127.0.0.1 ::1
# 设置数据库密码
requirepass 123456 

3.5 案例: 服务调用次数控制

人工智能领域的语义识别与自动对话将是未来服务业机器人应答呼叫体系中的重要技术, 百度自研用户评价语义识别服务, 免费开放企业试用, 同时训练百度自己的模型, 现对试用用户的使用行为进行限速, 限制每个用户每分钟最多发起10次调用.

案例要求:

(1) 设定A, B ,C 三个用户

(2) A用户限制10次/分调用, B用户限制30次/分调用, C用户不限制.

需求分析:

  • 设定一个服务方法, 用于模拟实际业务调用的服务, 内部采用打印模拟调用
  • 在业务调用前服务调用控制单元, 内部使用redis进行控制, 参照之前的方案
  • 对调用超限使用异常进行控制, 异常处理设定为打印提升信息
  • 主程序启动3个线程, 分别表示3种不同用户的调用

代码实现

@Service
public class BaiduServiceImpl implements BaiduService {
    @Autowired
    private JedisUtils jedisUtils;

    // 业务操作
    @Override
    public void business(String level,long num) {
        System.out.println("用户"+level+": 业务操作执行第"+num+"次");
    }

    // 控制单元
    @Override
    public void control(String level, int num) {

        Jedis jedis = jedisUtils.getJedis();
        String key = "compid:" + level;
        String value = jedis.get(key);
        try {
            // 判断该值是否存在
            if (value==null) {
                // 不存在, 创建该值
                jedis.setex(key, 20,String.valueOf(Long.MAX_VALUE-10));
            }else {
                // 存在, 自增, 调用业务
                Long incr = jedis.incr(key);
                business(level,num-(Long.MAX_VALUE-incr));
            }

        } catch (JedisDataException e) {
            System.out.println("用户"+level+"使用已经达到次数上限,请升级会员级别");
        }finally {
            jedis.close();
        }

    }
}

测试

@Test
public void testBaiduService() throws InterruptedException {

	Thread t1 = new Thread(() -> {
		for (;;){
			baiduService.control("level1",10);
			try {
				SECONDS.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	});

	Thread t2 = new Thread(() -> {
		for (;;){
			baiduService.control("level2",30);
			try {
				SECONDS.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	});

	t1.start();
	t2.start();
	t1.join();
	t2.join();
}

4. 基于Linux环境安装Redis

4.1 下载安装

下载安装包

wget http://download.redis.io/releases/redis-4.0.0.tar.gz

解压

tar -xvf redis-4.0.0.tar.gz

编译

make

安装

make install

4.2 启动Redis

指定端口启动Redis

./bin/redis-server --port=6379

查看redis.conf , 过滤掉注释信息(#)和空行 , 并输出到redis-6379.conf

cat redis.conf | grep -v "#" | grep -v "^$" > redis-6379.conf

指定配置文件启动Redis

./bin/redis-server redis-6379.conf

启动Redis客户端

./bin/redis-cli

4.3 Redis服务配置

基本配置

daemonize yes 以守护线程方式启动, 该方式下Redis将以服务的形式存在, 日志将不再打印到命令窗口中.

port 6379 设置当前服务启动端口号

dir /usr/local/redis/data 设定当前服务文件保存位置, 包含日志文件, 持久化文件等.

logfile "6379.log" 设定日志文件名, 便于查阅.

完整的配置项, 请移步到redis.conf文件查看.

5. 持久化

5.1 持久化简介

什么是持久化

利用永久性存储介质将数据进行保存, 在特定的时间将保存的数据进行恢复的工作机制称为持久化.

为什么要进行持久化

防止数据的意外丢失, 确保数据安全性.

持久化过程保存什么

  • RDB, 数据(快照) 01011100100这样的二进制数据
  • AOF, 操作过程(日志)

5.2 RDB

5.2.1 save指令

RDB启动方式----save指令相关配置

即时保存数据, 使用save指令手动执行一次保存操作.

  • dbfilename dump.rdb

    说明: 设置本地数据库文件名, 默认值为 dump.rdb ; 通常设置为 dump-端口号.rdb

  • dir

    说明: 设置存储.rdb文件的路径, 通常设置成存储空间较大的目录中, 目录名称为 data.

  • rdbcompression yes

    说明: 设置存储至本地数据库时是否压缩数据, 默认为yes, 采用LZF压缩. 通常默认为开启状态, 如果设置为no, 可以节省CPU运行时间, 但会使存储的文件变大.

  • rdbchecksum yes

    说明: 设置是否进行RDB文件格式校验, 校验过程在写文件和读文件过程均进行; 通常默认为开启状态, 如果设置为no, 可以节约读写过程约10%的时间消耗, 但是存在一定的数据损坏风险.

RDB启动方式----save指令工作原理
在这里插入图片描述

注意: save指令的执行会阻塞当前redis服务器, 直到当前RDB过程完成为止, 有可能造成长时间阻塞, 线上环境不建议使用.

5.2.2 bgsave指令

数据量过大, 单线程执行方式造成效率过低, 选择后台执行的bgsave方式, 手动启动后台保存操作, 但不是立即执行.

redis操作者(用户)发起指令, redis服务器控制指令执行, 做到即时发起, 合理的时间执行, 达到保存数据的效果.

RDB启动方式----bgsave指令相关配置

  • dbfilename dump.rdb

  • dir

  • rdbcompression yes

  • rdbchecksum yes

  • stop-writes-on-bgsave-error yes

    说明: 后台存储过程中如果出现错误现象, 是否停止保存操作, 通常默认为开启状态.

RDB启动方式----bgsave指令工作原理
在这里插入图片描述

注意: bgsave命令是针对save阻塞问题做的优化, Redis内部所有涉及到RDB操作都采用bgsave的方式, save命令可以放弃使用.

5.2.3 RDB启动方式

RDB相关配置

反复执行保存指令, 忘记了怎么办? 不知道数据产生了多少变化, 如何保存? Redis配置文件可以配置自动执行.

save second changes 满足限定时间范围内key的变化数量达到指定数量即进行持久化. second表示监控时间范围, changes表示监控key的变化量.

# save 3600 1
# save 300 100
# save 60 10000

save配置原理
在这里插入图片描述

注意:

(1) save配置要根据实际业务情况进行设置, 频度过高或过低都会出现性能问题, 结果可能是灾难性的.

(2) save配置中对于second与changes设置通常具有互补对应关系, 尽量不要设置成包含性关系.

(3) save配置启动后执行的是bgsave操作

5.2.4 RDB三种启动方式对比

在这里插入图片描述

5.2.5 RDB特殊启动形式

  • 全量复制 , 在主从复制中详细介绍

  • 服务器运行过程中重启

    debug reload

  • 关闭服务器时指定保存数据

    shutdown save

5.2.6 RDB优缺点

RDB优点

  • RDB是一个紧凑压缩的二进制文件, 存储效率较高.
  • RDB内部存储的是redis在某个时间点的数据快照, 非常适合用于数据备份, 全量复制等场景.
  • RDB恢复数据的速度要比AOF快很多
  • 应用: 服务器中每X小时执行bgsave备份, 并将RDB文件拷贝到远程服务器中, 用于灾难备份.

RDB缺点

  • RDB方式无论是执行指令还是利用配置, 无法做到实时持久化, 具有较大的可能性丢失数据. (宕机带来的数据丢失风险)
  • bgsave指令每次运行要执行fork操作创建子进程, 要牺牲一些性能. (fork子进程带来的额外内存消耗)
  • Redis的众多版本中未进行RDB文件格式的版本统一, 有可能出现各版本之间数据格式无法兼容问题.
  • 基于快照思想, 每次读写都是全部数据, 存储数据量较大时, 效率较低.

5.3 AOF

针对RDB存储的弊端, 可以从下面的思路解决:

  • 不写全量数据, 仅记录部分数据.
  • 改记录数据为记录操作过程.

5.3.1 AOF概念

  • AOF (append only file)持久化: 以独立日志的方式 记录每次写命令, 重启时再重新执行AOF文件中的命令, 达到恢复数据的目的. 与RDB相比可以简单描述为改记录数据为记录数据产生的过程.
  • AOF的主要作用是解决了数据持久化的实时性, 目前已经是Redis持久化的主流方式.

5.3.2 AOF写数据过程

在这里插入图片描述

5.3.3 AOF写数据三种策略

  • always

    每次写入操作均同步到AOF文件中, 数据零误差, 性能较低, 不建议使用.

  • everysec

    每秒缓冲区中的指令同步到AOF文件中, 数据准确性较高, 性能较高. 建议使用, 也是默认配置.

    在系统突然宕机的场景下会丢失1s内的数据.

  • no

    由操作系统控制每次同步到AOF文件的周期, 整体过程不可控.

5.3.4 AOF功能开启

redis.conf配置文件中配置appendonly yes|no, 表示是否开启AOF持久化功能, 默认为不开启状态.

redis.conf配置文件中配置appendfsync always|everysec|no, AOF写数据策略.

appendfilename filename 配置AOF持久化文件名, 默认为appendonly.aof , 建议配置为 appendonly-端口号.aof.

dir 配置AOF持久化文件保存路径, 与RDB持久化文件保持一致即可.

5.3.5 AOF重写

AOF写数据遇到的问题

如果连续执行如下指令该如何处理?
在这里插入图片描述

随着命令不断写入AOF, 文件会越来越大, 为了解决这个问题, Redis引入了AOF重写机制压缩文件体积, AOF文件重写是将Redis进程内的数据转化为写命令同步到新AOF文件的过程. 简单说就是将对同一个数据的若干条命令执行结果转化成最终结果数据对应的指令进行记录.

AOF重写作用

  • 降低磁盘占用量, 提高磁盘利用率.
  • 提高持久化效率, 降低持久化写时间, 提高IO性能.
  • 降低数据恢复的耗时, 提高数据恢复效率.

AOF重写规则

  • 进程内已超时的数据不再写入文件.

  • 忽略无效指令, 重写时使用进程内数据直接生成, 这样新的AOF文件只保留最终数据的写入命令.

    如: del key1, hdel key2 , srem key3, set key4 value4 等…

  • 对同一数据的多条写命令合并为一条命令

    如: lpush list1 a, lpush list1 b, lpush list1 c 可以转化为 lpush list1 a b c

    为防止数据量过大造成客户端缓冲区溢出, 对list, set, hash, zset等类型, 每条指令最多写入64个元素.

AOF重写方式

  • 手动重写 bgrewriteaof
  • 自动重写
    • auto-aof-rewrite-percentage percentage
    • auto-aof-rewrite-min-size size

AOF自动重写方式
在这里插入图片描述

5.3.6 AOF工作流程

在这里插入图片描述

AOF重写流程
在这里插入图片描述

5.3.7 RDB与AOF对比

在这里插入图片描述

RDB与AOF的选择
在这里插入图片描述

5.3.8 持久化应用场景

在这里插入图片描述

6. Redis事务

6.1 事务简介

什么是事务?

Redis执行指令过程中, 多条连续执行的指令被干扰, 打断, 插队.
在这里插入图片描述

Redis事务就是一个命令执行的队列, 将一系列预定义命令包装成一个整体(一个队列). 当执行时, 一次性按照添加顺序依次执行, 中间不会被打断或者干扰.

6.2 事务的基本操作

  • 开启事务

    multi 设定事务的开始位置, 此指令执行后, 后续的所有指令均加入到事务中.

  • 执行事务

    exec 设定事务的结束位置, 同时执行事务. 与multi成对出现, 成对使用.

注意: 加入事务的命令暂时进入到任务队列中, 并没有立即执行, 只有执行exec命令才开始执行.

  • 取消事务

    discard 终止当前事务的定义, 发生在multi之后, exec之前.

6.3 事务的工作流程

在这里插入图片描述

6.4 事务的注意事项

在这里插入图片描述

注意: 已经执行完毕的命令对应的数据不会自动回滚, 需要程序员自己在代码中实现回滚.

手动进行事务回滚:
在这里插入图片描述

6.5 锁的应用

基于特定条件的事务执行

6.5.1 业务场景1

天猫双11热卖过程中, 对已经售罄的货物追加补货, 4个业务员都有权限进行补货, 补货的操作可能是一系列的操作, 牵扯到多个连续操作, 如何保障不会重复操作?

业务分析:

  • 多个客户端有可能同时操作同一组数据, 并且该数据一旦被操作修改后 , 将不适用于继续操作
  • 在操作之前锁定要操作的数据, 一旦发生变化, 终止当前操作

解决方案

  • 对key添加监视锁, 在执行exec前如果key发生了变化, 终止事务执行.

    watch key1 [key2...]

  • 取消对所有key的监视

    unwatch

Tips: redis应用基于状态控制的批量任务执行.

6.5.2 业务场景2

天猫双11热卖过程中, 对已经售罄的货物追加补货, 且补货完成. 客户购买热情高涨, 3s内将所有商品购买完毕, 本次补货已经将全部库存清空, 如何避免最后一件商品不被多人同时购买? [超卖问题]

业务分析

  • 使用watch监控一个key有没有改变已经不能解决问题, 此处要监控的是具体数据.
  • 虽然redis是单线程的, 但是多个客户端对同一数据同时进行操作, 如何避免不被同时修改 ?

解决方案

  • 使用setnx设置一个公共锁 (分布式锁的使用)

    setnx lock-key value 利用setnx命令的返回值特征, 返回0设置失败, 返回1则设置成功.
    对于返回设置成功的, 拥有控制权, 进行下一步的具体业务操作.
    对于返回设置失败的, 不具有控制权, 排队等待.

  • 操作完毕通过del操作释放锁.

127.0.0.1:6379> set num 10 # 初始化数据
OK
127.0.0.1:6379> setnx lock-num 1 # 当前用户获取锁
(integer) 1
127.0.0.1:6379> incrby num -1 # 当前用户操作数据
(integer) 9
127.0.0.1:6379> del dock-num # 当前用户释放锁

注意: 上述方案是一种设计概念, 依赖规范保障, 具有风险性.

Tips: redis应用基于分布式锁对应的场景控制.

6.5.3 业务场景3

依赖分布式锁的机制, 某个用户操作时对应客户端宕机了, 并且此时已经获取到锁, 没有释放, 如何解决?

业务分析

  • 由于锁操作由用户控制加锁解锁, 必定会存在加锁后未解锁的风险.
  • 需要解锁操作不能仅依赖用户控制, 系统级别要给出对应的兜底解决方案.

解决方案

  • 使用 expire 为锁 key 添加时间限定, 超时没有释放, 就主动失效, 放弃锁. (分布式锁)

    expire lock-key second

    pexpire lock-key millseconds

## 客户端1模拟获取锁, 然后宕机了, 锁没有释放
127.0.0.1:6379> set name crysw # 数据初始化
OK
127.0.0.1:6379> set lock-name 1 # 设置锁
OK 
127.0.0.1:6379> expire lock-name 30 # 设置锁的有效期
(integer) 1
127.0.0.1:6379> set name pathd
OK
127.0.0.1:6379> ## 到这里宕机了!!!!! 锁还没有释放呢,咋办呢? 别急, 上面设置了锁的有效期.
## 客户端2模拟获取上面的锁, 一直获取失败, 直到锁失效后, 才成功设置锁
127.0.0.1:6379> setnx lock-name 1
(integer) 0
127.0.0.1:6379> setnx lock-name 1
(integer) 0
127.0.0.1:6379> setnx lock-name 1
(integer) 0
127.0.0.1:6379> setnx lock-name 1
(integer) 0
127.0.0.1:6379> ttl lock-name ## 查看锁的剩余时间, 已经失效, 下面重新设置锁
(integer) -2
127.0.0.1:6379> setnx lock-name 1
(integer) 1
127.0.0.1:6379>

由于操作通常都是微妙或毫秒级, 因此该锁定时间不宜设置过大. 具体时间需要根据业务逻辑测试后确认.

  • 例如: 持有锁的操作最长执行时间127ms, 最短执行时间7ms.
  • 测试百万次最长时间对应命令的最大耗时, 测试百万次网络延迟平均耗时.
  • 锁时间设定推荐: 最大耗时*120% + 平均网络延迟*110%
  • 如果业务最大耗时<<网络平均延迟, 通常为2个数量级, 取其中单个耗时较长即可.

7. 删除策略

7.1 过期数据

Redis是一种内存级数据库, 所有数据均存放在内存中, 内存中的数据可以通过TTL指令获取其状态.

  • XX: 具有时效性的数据
  • -1: 永久有效的数据
  • -2: 已经过期的数据或被删除的数据, 或未定义的数据.

7.2 数据删除策略

7.2.1 时效性数据的存储结构

在这里插入图片描述

数据删除策略的目标

在内存占用与CPU占用之间寻找一种平衡, 顾此失彼都会造成整体Redis性能的下降, 甚至引发服务器宕机或内存泄漏.

7.2.2 定时删除

  • 创建一个定时器, 当key设置有过期时间, 且过期时间到达时, 由定时器任务立即执行对键的删除操作.
  • 优点: 节约内存, 到时间就删除, 快速释放掉不必要的内存占用.
  • 缺点: CPU压力很大, 无论CPU此时负载量多高, 均占用CPU, 会影响redis服务器响应时间和指令吞吐量.
  • 总结: 用处理器性能换取存储空间( 拿时间换空间 ).

7.2.3 惰性删除

  • 数据到达过期时间, 不做处理, 等下次访问该数据时:
    • 如果未过期, 返回数据
    • 发现已过期, 删除key , 返回不存在
  • 优点: 节约CPU性能, 发现必须删除的时候才删除
  • 缺点: 内存压力很大, 出现长期占用内存的数据
  • 总结: 用存储空间换取处理器 ( 拿空间换时间 )

7.2.4 定期删除

以上两种方案都走极端, 有没有折中方案呢? 那就是定期删除

  • 当Redis启动服务器初始化时, 读取配置server.hz的值, 默认为 10 .
  • 每秒执行server.hz次serverCron() -> databasesCron() -> activeExpireCycle()
  • activeExpireCycle() 对每个expire[*]逐一进行检测, 每次执行 250ms/server.hz
  • 对某个expires[*]检测时, 随机挑选W个key检测:
    • 如果key超时, 删除key.
    • 如果一轮中删除的key的数量 大于 W*25%, 循环该过程.
    • 如果一轮中删除的key的数量小于等于 W*25%, 检查下一个expires[*], 0-15循环.
    • W取值 = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP属性值.
  • 参数current_db用于记录activeExpireCycle()进入expires[*]执行.
  • 如果activeExpireCycle()执行时间到期, 下次从current_db继续向下执行.

定期删除

  • 周期性轮询redis库中的时效性数据, 采取随机抽取的策略, 利用过期数据占比的方式控制删除频度.
  • 特点1: CPU性能占用设置有峰值, 检测频度可自定义设置.
  • 特点2: 内存压力不是很大, 长期占用内存的冷数据会被持续清理.
  • 总结: 周期性抽查存储空间 (随机抽查, 重点抽查).

7.2.5 删除策略比对

在这里插入图片描述

7.3 逐出算法

当新数据进入redis时, 如果内存不足怎么办 ?

Redis使用内存存储数据, 在执行每一个命令前, 会调用freeMemoryIfNeeded()检测内存是否充足. 如果内存不满足新加入数据的最低存储要求, redis要临时删除一些数据为当前指令清理存储空间. 清理数据的策略称为逐出算法.

注意: 逐出数据的过程不是100%能够清理出可使用的内存空间, 如果不成功则反复执行, 当对所有数据尝试完毕后, 如果不能达到内存清理的要求, 将出现错误信息.

(error)OOM command not allowed when used memory > 'maxmamory'

影响数据逐出的相关配置

  • 最大可使用内存 maxmemory, 占用物理内存的比例, 默认值为0 , 表示不限制. 生产环境中根据需求设定, 通常设置在50%以上.

  • 每次选取待删除数据的个数 maxmemory-samples , 选取数据时并不会全库扫描, 导致严重的性能消耗, 降低读写性能. 因此采用随机获取数据的方式作为检测删除数据.

  • 删除策略 maxmemory-policy , 达到最大内存后, 对被挑选出来的数据进行删除的策略.

  • 检测易失数据(可能会过期的数据集 server.db[i].expires)

    • volatile-lru 挑选最近最少使用的数据淘汰
    • volatile-lfu 挑选最近使用次数最少的数据淘汰
    • volatile-ttl 挑选将要过期的数据淘汰
    • volatile-random 任意选择数据淘汰

在这里插入图片描述

  • 检测全库数据( 所有数据集 server.db[i].dict )

    • allkeys-lru 挑选最近最少使用的数据淘汰
    • allkeys-lfu 挑选最近使用次数最少的数据淘汰
    • allkeys-random 任意选择数据淘汰
  • 放弃数据驱逐

    • no-enviction (驱逐) : 禁止驱逐数据 (redis4.0中默认策略), 会引发错误OOM.
maxmemory-policy volatile-lru

数据逐出策略配置依据

使用info命令输出监控信息, 查询缓存hit和miss的次数, 根据业务需求调优redis配置.

8. redis.conf

8.1 服务器基础配置

服务器端设定

  • 设置服务器以守护进程的方式运行

    daemonize yes|no

  • 绑定主机地址, 如果绑定了, 其他客户端只能通过绑定的ip访问服务.

    bind 127.0.0.1

  • 设置服务器端口号

    port 6379

  • 设置数据库数量

    databases 16

日志配置

  • 设置服务器以指定日志记录级别

    loglevel debug | verbose | notice | warning

  • 日志记录文件名

    logfile 端口号.log

注意: 日志级别开发中设置为verbose, 生产环境设置为notice, 简化日志输出量, 降低日志IO的频度.

客户端配置

  • 设置同一时间最大客户端连接数, 默认无限制. 当客户端连接到达上限, Redis会关闭新的连接.

    maxclients 0

  • 客户端闲置等待最大时长, 达到最大值后关闭连接, 如需关闭该功能, 设置为0.

    timeout 300

多服务器快捷配置

  • 导入并加载指定配置文件信息, 用于快速创建redis公共配置较多的redis实例配置文件, 便于维护.

    include /path/server-端口号.conf

9. 高级数据类型

9.1 Bitmaps

基础操作

  • 获取指定key对应偏移量上的bit值

    getbit key offset

  • 设置指定key对应偏移量上的bit值, value只能是1或0.

    setbit key offset value

扩展操作

  • 对指定key按位进行交, 并, 非, 异或操作, 并将结果保存到destKey中. and, or, not , xor

    bitop or destkey key1 [key2...]

  • 统计指定key中1的数量

    bitcount key [start end]

业务场景

电影网站

  • 统计每天某一部电影是否被点播
  • 统计每天有多少部电影被点播
  • 统计每周/月/年有多少部电影被点播
  • 统计年度哪部电影没有被点播

业务分析

维护bit位…

9.2 HyperLogLog

统计独立UV

  • 原始方案: set, 存储每个用户的id (字符串)
  • 改进方案: Bitmaps, 存储每个用户状态 (bit)
  • 全新的方案: Hyperloglog

Hyperloglog 用于做基数统计

{1, 3, 5, 7, 5, 7, 8} > 基数集: {1,3,5,7,8} 基数: 5

{1, 1, 1, 1, 1, 1, 7, 1} > 基数集: {1,7} 基数: 2

HyperLogLog的基本操作

  • 添加数据

    pfadd key element [element...]

  • 统计数据

    pfcount key [key...]

  • 合并数据

    pfmerge destkey sourcekey [sourcekey...]

相关说明

  • 用于进行基数统计, 不是集合, 不保存数据, 只记录数量而不是具体数据
  • 核心是基数估算算法, 最终数值存在一定误差
  • 误差范围: 基数估计的结果是一个带有 0.81% 标准错误的近似值.
  • 耗空间极小 , 每个HyperLogLog key占用了12k的内存用于标记基数
  • pfadd命令不是一次性分配12k内存使用, 会随着基数的增加内存逐渐增大
  • pfmerge命令合并后占用的存储空间为12k, 无论合并之前数据量有多大.

9.3 GEO

用于地理位置计算.

基本操作

  • 添加坐标点

    geoadd key longitude latitude member [longitude latitude member...]

  • 获取坐标点

    geopos key member [member...]

  • 计算坐标点距离

    geodist key member1 member2 [unit]
    在这里插入图片描述

10 主从复制

互联网三高架构: 高并发, 高性能, 高可用.

业务可用性目标5个9, 即 99.999%, 服务器年宕机时长低于315s, 约5.25分钟. 如何计算可用性比例呢? 比如:

服务宕机实际年宕机时长866467s , 可用性=(1*365*24*60*60 - 866467) * 100% / 1*365*24*60*60 = 97.252%

10.1 简介

在这里插入图片描述

10.2 多台服务器连接方案

在这里插入图片描述

主从复制

主从复制, 即将master中的数据即时, 有效的复制到slave中.

特征: 一个master可以拥有多个slave, 一个slave只对应一个master.

职责:

  • master , 写数据; 执行写操作时, 将出现变化的数据自动同步到slave; 读数据(可忽略).
  • slave, 读数据, 禁止写数据.

作用:

  • 读写分离, master写, slave度, 提高服务器的读写负载能力.
  • 负载均衡, 基于主从结构, 配合读写分离, 由slave分担master负载, 并根据需求的变化, 改变slave的数量, 通过多个从节点分担数据读取负载, 大大提高Redis服务器并发量与数据吞吐量.
  • 故障恢复, 当master出现问题时, 由slave提供服务, 实现快速的故障恢复.
  • 数据冗余, 实现数据热备份, 是持久化之外的一种如今冗余方式.
  • 高可用, 基于主从复制, 构建哨兵模式与集群, 实现Redis的高可用方案.

10.3 主从复制工作流程

主从复制的过程大体可以分为三个阶段:

  • 建立连接阶段(准备阶段)
  • 数据同步阶段
  • 命令传播阶段

在这里插入图片描述

10.3.1 阶段一: 建立连接阶段工作流程

在这里插入图片描述

主从连接(slave 连接 master)

  • 方式一: 客户端发送命令 slaveof <masterip> <masterport>

  • 方式二: 启动服务器参数 ./redis-server --slaveof <masterip> <masterport>

  • 方式三: 服务器配置 slaveof <masterip> <masterport>

  • 方式四: 集群配置

    # 三主三从
    ./src/redis-cli --cluster create -a 123456 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 --cluster-replicas 1
    

主从断开连接

  • 客户端发送命令 slaveof no one

授权访问

  • master配置文件设置密码 requirepass <password>

  • master客户端发送命令设置密码

    config set requirepass <password>

    config get requirepass

  • slave客户端发送命令设置密码 auth <password>

  • slave配置文件设置密码 masterauth <password>

  • 启动客户端设置密码 redis-cli -a <password>

10.3.2 阶段二: 数据同步阶段工作流程

  • 在slave初次连接master后, 复制master中的所有数据到slave.
  • 将slave的数据库状态更新成master当前的数据库状态.

在这里插入图片描述

数据同步阶段master说明

  • 如果master数据量巨大, 数据同步阶段应该避开流量高峰期, 避免造成master阻塞, 影响业务正常执行.

  • 复制缓冲区大小设定不合理, 会导致数据溢出. 如进行全量复制周期太长, 进行部分复制时发现数据已经存在丢失的情况, 必须进行第二次全量复制, 致使slave陷入死循环状态. 如何解决呢? 修改复制缓冲区大小.

    repl-backlog-size 1mb

数据同步阶段slave说明

  • 为避免slave进行全量复制, 部分复制时服务器响应阻塞或数据不同步, 建议关闭此期间的对外服务.

    slave-serve-stale-data yes|no

  • 数据同步阶段, master发送给slave信息可以理解master是slave的一个客户端, 主动向slave发送命令.

  • 多个slave同时对master请求数据同步, master发送的RDB文件增多, 会对带宽造成巨大冲击, 以放master带宽不足, 数据同步需要根据业务需求, 适量错峰.

  • slave过多时, 建议调整拓扑结构, 由一主多从结构变为树状结构, 中间的节点既是master, 也是slave. 注意使用树状结构时, 由于层级深度, 导致深度越高的slave与最顶层master数据同步延迟较大, 数据一致性变差, 应谨慎选择.

10.3.3 阶段三: 命令传播阶段

  • 当master数据库被修改后, 导致主从服务器数据库状态不一致, 此时需要让主从数据同步到一致的状态, 同步的动作称为命令传播.
  • master将接收到的数据变更命令发送给slave, slave接收到命令后执行.

命令传播阶段的部分复制

命令传播阶段出现了断网现象

  • 网络闪断闪连, 进行忽略
  • 短时间网络中断, 部分复制
  • 长时间网络中断, 全量复制

部分部分的三个核心要素

  • 服务器的运行ID(run id)
  • 主服务器的复制积压缓冲区
  • 主从服务器的复制偏移量

服务器运行ID (runid)

  • 概念: 服务器运行ID是每一台服务器每次运行的身份识别码, 一台服务器多次运行可以生成多个运行ID.

  • 组成: 运行ID由40位字符组成, 是一个随机的十六进制字符.

  • 作用: 运行ID被用于在服务器间进行传输, 识别身份; 如果想两次操作均对同一台服务器进行, 必须每次操作携带对应的运行ID , 用于识别对方.

  • 实现方式: 运行ID在每台服务器启动时自动生成的, master在首次连接slave时, 会将自己的运行ID发给slave, slave保存运行ID, 通过info server命令可以查看.

复制缓冲区

  • 概念: 复制积压缓冲区, 是一个先进先出(FIFO)的队列, 用于存储服务器执行过的命令, 每次传播命令, master都会将传播的命令记录下来, 并存储在复制缓冲区.

    复制缓冲区默认数据存储空间大小是1M, 由于存储空间大小是固定的, 当入队元素的数量大于队列长度时, 最先入队的元素会被弹出, 而新元素会被放入队列.

  • 由来: 每台服务器启动时, 如果开启有AOF或被连接成为master节点, 即创建复制缓冲区.

  • 作用: 用于保存master收到的所有指令 (仅影响数据变更的指令, 例如set)

  • 数据来源: 当master接收到主客户端的指令时, 除了将指令执行, 会将该指令存储到缓冲区中.

在这里插入图片描述

复制缓冲区内部工作原理
在这里插入图片描述

主从服务器复制偏移量 (offset)

  • 概念: 一个数字, 描述复制缓冲区中的指令字节位置.
  • 分类
    • master复制偏移量: 记录发送给所有slave的指令字节对应的位置(多个)
    • slave复制偏移量: 记录slave接收master发送过来的指令字节对应的位置( 一个)
  • 数据来源
    • master端: 发送一次记录一次
    • slave端: 接收一次记录一次
  • 作用: 同步信息, 比对master与slave的差异, 当slave断线后, 恢复数据使用.

10.3.4 工作流程

在这里插入图片描述



在这里插入图片描述

10.3.5 心跳机制

  • 进入命令传播阶段后, master与slave之间需要进行信息交换, 使用心跳机制进行维护, 实现双方连接保持在线.
  • master心跳
    • 指令: ping
    • 周期: 由repl-ping-slave-period决定, 默认10s
    • 作用: 判断slave是否在线
    • 查询: info replication , 获取slave最后一次连接时间间隔, lag项维持在0或1视为正常.
  • slave心跳任务
    • 指令: replconf ack {offset}
    • 周期: 1s
    • 作用: 汇报slave字节的复制偏移量, 获取最新的数据变更指令; 并且可以判断master是否在线.

心跳阶段注意事项:

  • 当slave多数掉线, 或延迟过高时, master为保障数据稳定性, 将拒绝所有信息同步操作

    min-slaves-to-write 2

    min-slaves-max-lag 8

    slave数量少于2个, 或者所有slave的延迟都大于等于10s时, 强制关闭master写功能, 停止数据同步.

  • slave数量由slave发送replconf ack命令做确认

  • slave延迟由slave发送replconf ack命令做确认

10.4 常见问题

10.4.1 频繁的全量复制(1)

伴随着系统的运行, master的数据量会越来越大, 一旦master重启, runid将发生变化, 会导致全部slave的全量复制操作.

内部优化调整方案

(1) master内部创建master_repid变量, 使用runid相同的策略生成, 长度41位, 并发送给所有slave.

(2) 在master关闭时执行命令 shutdown save, 进行RDB持久化, 将runid与offset保存到RDB文件中.

  • repl-id repl-offset
  • 通过redis-check-rdb命令可以查看该信息.

(3) master重启后加载RDB文件, 恢复数据. 重启后, 将RDB文件中保存的repl-id 与 repl-offset加载到内存中.

  • master_repl_id=repl master_repl_offset=repl-offset
  • 通过info命令可以查看该信息

作用: 本机保存上次runid, 重启后恢复该值, 使所有slave认为还是之前的master.

10.4.2 频繁的全量复制(2)

问题现象: 网络环境不佳, 出现网络中断, slave不提供服务.

问题原因: 复制缓冲区大小, 断网后slave的offset越界, 触发全量复制.

最终结果: slave反复进行全量复制.

解决方案: 修改复制缓冲区大小. repl-backlog-size

建议设置如下:

  • 测算从master到slave的重连平均时长second
  • 获取master平均每秒产生写命令数据总量 write_size_per_second
  • 最优复制缓冲区空间 = 2*second*write_size_second

10.4.3 频繁的网络中断(1)

问题现象: master的CPU占用过高或slave频繁断开连接.

问题原因:

  • slave每1s发送replconf ack命令到master.
  • 当slave接到了慢查询时 (keys *, hgetall等), 会大量占用CPU性能.
  • master每1s调用复制定时函数replicationCron(), 比对slave发现长时间没用进行响应.

最终结果: master各种资源 (输出缓冲区, 带宽, 连接等) 被严重占用.

解决方案: 通过设置合理的超时时间, 确认是否被释放 slave.

repl-timeout 该参数定义了超时时间的阈值 (默认60s), 超过该值, 释放slave.

10.4.4 频繁的网络中断(2)

问题现象: slave与master连接断开

问题原因:

  • master发送ping指令频度较低.
  • master设定超时时间较短
  • ping指令在网络中存在丢包

解决方案: 提高ping指令发送的频度.

repl-ping-slave-period 超时时间repl-time的时间至少是ping指令频度的5到10倍, 否则slave很容易判定超时.

10.4.5 数据不一致

问题现象: 多个slave获取相同数据不同步

问题原因: 网络信息不同步, 数据发送有延迟

解决方案:

  • 优化主从间的网络环境, 通常放置在同一个机房部署, 如使用阿里云等云服务器时要注意此现象.

  • 监控主从节点延迟(通过offset)判断, 如果slave延迟过大, 暂时屏蔽程序对该slave的数据访问.

    slave-server-stale-data yes|no

    开启后仅响应info, slaveof等少数命令 (慎用, 除非对数据一致性要求很高).

11. 哨兵模式

11.1 哨兵简介

发生场景: 将宕机的master下线, 找一个slave作为master, 通知所有的slave连接新的master, 启动新的master与slave, 可能触发全量复制*N + 部分复制*N .

哨兵(sentinel) 是一个分布式系统, 用于对主从结构中的每台服务器进行监控, 当出现故障时通过投票机制选择新的master, 并将所有slave连接到新的master.

哨兵的作用:

(1) 监控

  • 不断的检查master和slave是否正常运行
  • master存活检测, master与slave运行情况检测

(2) 通知(提醒) , 当被监控的服务器出现问题时, 向其他 (哨兵间, 客户端) 发送通知.

(3) 自动故障转移, 断开master与slave连接, 选取一个slave作为master, 将其他slave连接到新的master, 并告知客户端新的服务器地址.

注意: 哨兵也是一台redis服务器, 只是不提供数据服务, 通常哨兵配置数量为单数.

11.2 启用哨兵模式

配置哨兵

  • 配置一拖二的主从结构
  • 配置三个哨兵 (配置相同, 端口不同), 参看 sentinel.conf
[root@centos7-01 redis_cluster]# cat sentinel/sentinel-27000.conf
port 27000
daemonize yes
pidfile "/var/run/redis-sentinel.pid"
logfile "/usr/local/redis_cluster/data/sentinel-27000.log"
dir "/usr/local/redis_cluster/data"
sentinel monitor mymaster 127.0.0.1 7000 2 # 监听的master主机, 达到2个哨兵认为master不行了,就重新选举
  • 启动哨兵 redis-sentinel sentinel-端口号.conf
[root@centos7-01 redis_cluster]# ./src/redis-sentinel sentinel/sentinel-27000.conf
[root@centos7-01 redis_cluster]# ./src/redis-sentinel sentinel/sentinel-27001.conf
[root@centos7-01 redis_cluster]# ./src/redis-sentinel sentinel/sentinel-27002.conf
[root@centos7-01 redis_cluster]# ps -ef | grep redis-sentinel # 查看哨兵进程
root       2799      1  0 08:41 ?        00:02:23 /usr/local/redis_cluster/src/redis-sentinel *:27000 [sentinel]
root       2805      1  0 08:41 ?        00:02:25 /usr/local/redis_cluster/src/redis-sentinel *:27001 [sentinel]
root       2811      1  0 08:41 ?        00:02:37 /usr/local/redis_cluster/src/redis-sentinel *:27002 [sentinel]

也可以将多个哨兵的启动命令写到一个shell脚本中, 比如: startSentinel.sh; 然后 sh startSentinel.sh就可以完成哨兵的启动.

/usr/local/redis_cluster/src/redis-sentinel sentinel/sentinel-27000.conf
/usr/local/redis_cluster/src/redis-sentinel sentinel/sentinel-27001.conf
/usr/local/redis_cluster/src/redis-sentinel sentinel/sentinel-27002.conf

11.3 哨兵工作原理

哨兵进行主从切换过程中经历三个阶段

  • 监控
  • 通知
  • 故障转移

11.3.1 阶段一: 监控

用于同步各个节点的状态信息.

  • 获取各个sentinel的状态(是否在线);
  • 获取master的状态
    • master属性runid和 master_role
    • 各个slave的详细信息
  • 获取所有slave的状态 (根据master中的slave信息)
    • slave属性 (runid, slave_role, master_host, master_port, offset…)

在这里插入图片描述

11.3.2 阶段二: 通知

保持联通.
在这里插入图片描述

11.3.3 阶段三: 故障转移

发现问题, 竞选负责人, 优选新master, 新master上任, 其他slave切换master, 原master作为slave故障恢复后连接.

master发送故障

在这里插入图片描述

哨兵选举主事人
在这里插入图片描述

主事人(哨兵)挑选新的master

哨兵leader从服务器列表中挑选可用的master.

  • 在线的, 响应慢的, 与原来master断开时间久的
  • 优先原则 (优先级, offset, runid)

发送指令(sentinel)

  • 向新的master发送slaveof no one
  • 向其他slave发送slaveof新的master IP,端口.

在这里插入图片描述

12. 集群

12.1 集群简介

现状问题: 业务发展过程中遇到的峰值瓶颈

  • redis提供的服务OPS可以达到10万/s, 当前业务OPS已经达到20万/s.
  • 内存单机容量达到256G, 当前业务需求内存容量1T

使用集群的方式可以快速解决上述问题…

集群作用

  • 分散单台服务器的访问压力, 实现负载均衡.

  • 分散单台服务器的存储压力, 实现可扩展性.

  • 降低单台服务器宕机带来的业务灾难.

12.2 Redis集群结构设计

数据存储设计

  • 通过算法设计, 计算出key应该保存的位置.
  • 将所有的存储空间计划切割成16384份, 每台主机保存一部分, 每部分代表的是一个存储空间, 不是一个key的保存空间.
  • 将key按照计算出的结果放到对应的存储空间.
  • 增强可扩展性

集群内部通讯设计

  • 各个数据库相互通信, 保存各个库中槽的编号数据.

  • 一次命中, 直接返回.

  • 一次未命中, 告知具体位置.

在这里插入图片描述

12.3 cluster集群结构搭建

将解压包中的脚本src目录复制到指定集群目录

cp -r -p src /usr/local/redis_cluster2/

将解压包中的配置文件输出到指定目录

cat redis.conf > /usr/local/redis_cluster2/redis-6379.conf

配置文件中开启集群模式

cluster-enabled yes
cluster-config-file nodes-6379.conf
cluster-node-timeout 5000
dir /usr/local/redis_cluster2/data

cluster配置涵义介绍

  • cluster-enabled yes|no 设置加入cluster, 成为其中的节点
  • cluster-config-file <filename> cluster配置文件名, 该文件属于自动生成, 仅用于快速查找文件并查询文件内容
  • cluster-node-timeout <millseconds>节点服务响应超时时间, 用于判断该节点是否下线或切换为从节点
  • cluster-migration-barrier <count> master连接的slave最小数量.

复制多份配置

sed "s/6379/6380/g" redis-6379.conf > redis-6380.conf
sed "s/6379/6381/g" redis-6379.conf > redis-6381.conf
sed "s/6379/6382/g" redis-6379.conf > redis-6382.conf
sed "s/6379/6383/g" redis-6379.conf > redis-6383.conf
sed "s/6379/6384/g" redis-6379.conf > redis-6384.conf

启动集群(6个节点)

./src/redis-server /usr/local/redis_cluster2/redis-6379.conf
./src/redis-server /usr/local/redis_cluster2/redis-6380.conf
./src/redis-server /usr/local/redis_cluster2/redis-6381.conf
./src/redis-server /usr/local/redis_cluster2/redis-6382.conf
./src/redis-server /usr/local/redis_cluster2/redis-6383.conf
./src/redis-server /usr/local/redis_cluster2/redis-6384.conf

查看集群进程

[root@centos7-01 redis_cluster2]# ps -ef | grep redis
root       4234   3582  0 16:52 pts/0    00:00:03 ./src/redis-server *:6379 [cluster]
root       4242   3655  0 16:52 pts/1    00:00:03 ./src/redis-server *:6380 [cluster]
root       4248   3695  0 16:52 pts/2    00:00:03 ./src/redis-server *:6381 [cluster]
root       4258   4071  0 16:53 pts/3    00:00:03 ./src/redis-server *:6382 [cluster]
root       4432   4273  0 16:54 pts/4    00:00:02 ./src/redis-server *:6383 [cluster]
root       4442   4358  0 16:55 pts/6    00:00:02 ./src/redis-server *:6384 [cluster]
root       4587   4318  0 17:10 pts/5    00:00:00 grep --color=auto redis

通过redis-cli客户端命令来创建集群节点 (-a 是指定密码, requirepass没有配置,可以省略)

# 1 表示 1个master连接一个slave
./src/redis-cli --cluster create -a 123456 127.0.0.1:6379 127.0.0.1:6380 127.0.0.1:6381 127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6384 --cluster-replicas 1

或者通过redis-trib.rb创建集群节点( 需要具有ruby环境, ruby -v查看版本)

./src/redis-trib.rb create --replicas 1 127.0.0.1:6379 127.0.0.1:6380 127.0.0.1:6381 127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6384

查看启动集群日志

[root@centos7-01 redis_cluster2]# ./src/redis-cli --cluster create 127.0.0.1:6379 127.0.0.1:6380 127.0.0.1:6381 127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6384 --cluster-replicas 1
>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 127.0.0.1:6383 to 127.0.0.1:6379
Adding replica 127.0.0.1:6384 to 127.0.0.1:6380
Adding replica 127.0.0.1:6382 to 127.0.0.1:6381
>>> Trying to optimize slaves allocation for anti-affinity
[WARNING] Some slaves are in the same host as their master # 三主三从  M S
M: ac5d557520b285ea526416b09a87f199cf9c134a 127.0.0.1:6379
   slots:[0-5460] (5461 slots) master
M: a8bf9e21ea60fdf318913c988bb2019ef0d07c37 127.0.0.1:6380
   slots:[5461-10922] (5462 slots) master
M: 622ff152ca0d0786b09a82615cc8bfb92df8c2ad 127.0.0.1:6381
   slots:[10923-16383] (5461 slots) master
S: 20a55e126d518c828e502d0a85a6eceaa420e0b3 127.0.0.1:6382
   replicates 622ff152ca0d0786b09a82615cc8bfb92df8c2ad
S: 466bef9cb5cbbd04696fbc11eed96a1de1cc4c58 127.0.0.1:6383
   replicates ac5d557520b285ea526416b09a87f199cf9c134a
S: c40dd82542d3d0e1aa6c442f0055646f5deb8e14 127.0.0.1:6384
   replicates a8bf9e21ea60fdf318913c988bb2019ef0d07c37
Can I set the above configuration? (type 'yes' to accept): yes # 覆写配置文件内容
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join
.
>>> Performing Cluster Check (using node 127.0.0.1:6379)
M: ac5d557520b285ea526416b09a87f199cf9c134a 127.0.0.1:6379 # master 6379
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
S: 466bef9cb5cbbd04696fbc11eed96a1de1cc4c58 127.0.0.1:6383 # slave 6383
   slots: (0 slots) slave
   replicates ac5d557520b285ea526416b09a87f199cf9c134a
S: 20a55e126d518c828e502d0a85a6eceaa420e0b3 127.0.0.1:6382 # slave 6382
   slots: (0 slots) slave
   replicates 622ff152ca0d0786b09a82615cc8bfb92df8c2ad
S: c40dd82542d3d0e1aa6c442f0055646f5deb8e14 127.0.0.1:6384 # slave 6384
   slots: (0 slots) slave
   replicates a8bf9e21ea60fdf318913c988bb2019ef0d07c37
M: a8bf9e21ea60fdf318913c988bb2019ef0d07c37 127.0.0.1:6380  # master 6380
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
M: 622ff152ca0d0786b09a82615cc8bfb92df8c2ad 127.0.0.1:6381  # master 6381
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered. # 16384个槽点全部分配
[root@centos7-01 redis_cluster2]#

进入data目录查看生成的集群节点文件

[root@centos7-01 redis_cluster2]# cd data
[root@centos7-01 data]# ll
total 28
-rw-r--r--. 1 root root 175 Mar 19 17:10 dump.rdb
-rw-r--r--. 1 root root 781 Mar 19 17:10 nodes-6379.conf
-rw-r--r--. 1 root root 781 Mar 19 17:10 nodes-6380.conf
-rw-r--r--. 1 root root 781 Mar 19 17:10 nodes-6381.conf
-rw-r--r--. 1 root root 781 Mar 19 17:10 nodes-6382.conf
-rw-r--r--. 1 root root 781 Mar 19 17:10 nodes-6383.conf
-rw-r--r--. 1 root root 781 Mar 19 17:10 nodes-6384.conf

例如, 查看节点 nodes-6379.conf 的内容

## 查看节点的槽点slots分配
[root@centos7-01 data]# cat nodes-6379.conf
ac5d557520b285ea526416b09a87f199cf9c134a 127.0.0.1:6379@16379 myself,master - 0 1647681030000 1 connected 0-5460 # slots
466bef9cb5cbbd04696fbc11eed96a1de1cc4c58 127.0.0.1:6383@16383 slave ac5d557520b285ea526416b09a87f199cf9c134a 0 1647681031000 1 connected
20a55e126d518c828e502d0a85a6eceaa420e0b3 127.0.0.1:6382@16382 slave 622ff152ca0d0786b09a82615cc8bfb92df8c2ad 0 1647681031553 3 connected
c40dd82542d3d0e1aa6c442f0055646f5deb8e14 127.0.0.1:6384@16384 slave a8bf9e21ea60fdf318913c988bb2019ef0d07c37 0 1647681031247 2 connected
a8bf9e21ea60fdf318913c988bb2019ef0d07c37 127.0.0.1:6380@16380 master - 0 1647681031553 2 connected 5461-10922
622ff152ca0d0786b09a82615cc8bfb92df8c2ad 127.0.0.1:6381@16381 master - 0 1647681030220 3 connected 10923-16383
vars currentEpoch 6 lastVoteEpoch 0

查看dump.rdb文件内容 , 因为该文件是二进制, 需要借助命令redis-check-rdb

[root@centos7-01 data]# ../src/redis-check-rdb dump.rdb
[offset 0] Checking RDB file dump.rdb
[offset 26] AUX FIELD redis-ver = '6.2.6'
[offset 40] AUX FIELD redis-bits = '64'
[offset 52] AUX FIELD ctime = '1647681029'
[offset 67] AUX FIELD used-mem = '2716936'
[offset 85] AUX FIELD repl-stream-db = '0'
[offset 135] AUX FIELD repl-id = '0575a59a98fed22a85ff99b2c0b1a5164b71d57f'
[offset 150] AUX FIELD repl-offset = '0'
[offset 166] AUX FIELD aof-preamble = '0'
[offset 175] Checksum OK
[offset 175] \o/ RDB looks OK! \o/
[info] 0 keys read
[info] 0 expires
[info] 0 already expired

集群数据共享操作 , 集群下连接客户端 ./src/redis-cli -c -p 端口. 需要注意-c不能少.

[root@centos7-01 redis_cluster2]# ./src/redis-cli
127.0.0.1:6379> # 默认连接上6379客户端
127.0.0.1:6379> set name crysw # set操作失败, 说要去到 6380
(error) MOVED 5798 127.0.0.1:6380
127.0.0.1:6379>
127.0.0.1:6379> quit
[root@centos7-01 redis_cluster2]# ./src/redis-cli -c -p 6380  # 集群连接到6380客户端
127.0.0.1:6380> set name crysw # 设置操作ok
OK
127.0.0.1:6380> get name
"crysw"
127.0.0.1:6380> quit
[root@centos7-01 redis_cluster2]# ./src/redis-cli -c -p 6379  # 再次回到6379客户端
127.0.0.1:6379> get name  # get操作, 可以读取到6380的数据, 实现了数据共享.
-> Redirected to slot [5798] located at 127.0.0.1:6380
"crysw"
127.0.0.1:6380>

查看cluster节点信息

[root@centos7-01 redis_cluster2]# ./src/redis-cli -c -p 6379
127.0.0.1:6379>
127.0.0.1:6379> cluster nodes
ac5d557520b285ea526416b09a87f199cf9c134a 127.0.0.1:6379@16379 myself,master - 0 1647682485000 1 connected 0-5460
466bef9cb5cbbd04696fbc11eed96a1de1cc4c58 127.0.0.1:6383@16383 slave ac5d557520b285ea526416b09a87f199cf9c134a 0 1647682486000 1 connected
20a55e126d518c828e502d0a85a6eceaa420e0b3 127.0.0.1:6382@16382 slave 622ff152ca0d0786b09a82615cc8bfb92df8c2ad 0 1647682485000 3 connected
c40dd82542d3d0e1aa6c442f0055646f5deb8e14 127.0.0.1:6384@16384 slave a8bf9e21ea60fdf318913c988bb2019ef0d07c37 0 1647682486865 2 connected
a8bf9e21ea60fdf318913c988bb2019ef0d07c37 127.0.0.1:6380@16380 master - 0 1647682486559 2 connected 5461-10922
622ff152ca0d0786b09a82615cc8bfb92df8c2ad 127.0.0.1:6381@16381 master - 0 1647682486559 3 connected 10923-16383
127.0.0.1:6379>

12.4 cluster节点操作命令

cluster nodes 查看集群节点信息.

cluster replicate <master-id> 进入一个从节点redis, 切换其主节点.

cluster meet ip:port 发现一个新节点, 新增主节点.

cluster forget <id> 忽略一个没有slot的节点.

cluster failover 手动故障转移.

13. 企业级解决方案

13.1 缓存预热

服务器启动后迅速宕机, 问题排查:

  • 请求数量较高.

  • 主从之间的数据吞吐量较大, 数据同步操作频度较高.

解决方案:

前置准备工作:

  • 日常例行统计数据访问记录, 统计访问频度较高的热点数据.
  • 利用LRU数据删除策略, 构建数据留存队列. (storm与kafka配合)

准备工作:

  • 将统计结果中的数据分类, 根据级别, redis优先加载级别较高的热点数据.
  • 利用分布式多服务器同时进行数据读取, 提速数据加载过程.

实时:

  • 使用脚本程序固定触发数据预热过程
  • 如果条件允许, 使用了CDN(内容分发网络), 效果会更好.

总结:

缓存预热就是系统启动前, 提前将相关的缓存数据直接加载到缓存系统. 避免在用户请求的时候, 先查询数据库, 然后再将写入缓存的问题. 用户直接查询事先被预热的缓存数据.

13.2 缓存雪崩

数据库服务器崩溃, 以下几种:

  • 系统平稳运行过程中, 忽然数据库连接量激增.
  • 应用服务器无法及时处理请求.
  • 大量408, 500错误页面出现
  • 客户反复刷新页面获取数据
  • 数据库崩溃
  • 应用服务器崩溃
  • 重启应用服务器无效
  • Redis服务器崩溃
  • Redis集群崩溃
  • 重启数据库后再次被瞬间流量放倒.

问题排查

  • 在一个较短的时间内, 缓存中较多的key集中过期.
  • 此周期内请求访问过期的数据, redis未命中, redis向数据库获取数据.
  • 数据库同时接收到大量的请求无法及时处理.
  • Redis大量请求被积压, 开始出现超时现象.
  • 数据库流量激增, 数据库崩溃.
  • 重启后仍然面对缓存中无数据可用
  • Redis服务器资源被严重占用, Redis服务器崩溃
  • Redis集群崩塌, 集群瓦解
  • 应用服务器无法及时得到数据响应请求, 来自客户端的请求数量越来越多, 应用服务器崩溃
  • 应用服务器, redis, 数据库全部重启, 效果不理想.

解决方案(道), 设计角度

  • 更多的页面静态化处理
  • 构建多级缓存架构 (Nginx缓存 + redis缓存 + ehcache缓存)
  • 检测MySql严重耗时业务进行优化, 对数据库的瓶颈进行排查.
  • 灾难预警机制, 监控redis服务器性能指标 (CPU占用, CPU使用率, 内存容量, 查询平均响应时间, 线程数)
  • 限流, 降级. 短时间范围内牺牲一些客户体验, 限制一部分请求访问, 降低应用服务器压力, 待业务低速运转后再逐步放开访问.

解决方案(术), 实施角度

  • LRU与LFU切换
  • 数据有效期策略调整
    • 根据业务数据有效期进行分类错峰, A类90min, B类80min, C类70min
    • 过期时间使用固定时间 + 随机值的形式, 稀释集中到期的key的数量.
  • 超热数据使用永久key
  • 定期维护(自动 + 人工), 对即将过期的数据做访问量分析, 确认是否延时, 配合访问量统计做热点数据的延时.
  • 加锁 , 慎用.

总结

缓存雪崩就是瞬间过期数据量太大, 导致对数据库服务器造成压力, 如果能够有效避免过期时间集中, 可以有效解决雪崩现象的出现(约40%), 配合其他策略一起使用, 并监控服务器的运行数据, 根据运行记录做快速调整.

13.3 缓存击穿(热点key)

系统平稳运行过程中, 数据库连接瞬间激增, Redis服务器无大量key过期, Redis内存平稳, Redis服务器CPU正常, 热点key过期, 数据库服务器压力激增, 导致数据库崩溃.

问题排查

  • Redis中某个key过期, 该key访问量巨大.
  • 多个数据请求从服务器直接压到Redis后, 均未命中.
  • Redis在短时间内发起了大量对数据库中同一数据的访问.

问题分析: 单个key高热数据, 短时过期.

解决方案(术)

  • 预先设定, 以电商为例, 每个商家根据店铺等级, 指定若干款主打商品, 在购物节期间, 加大此类信息key的过期时长.
  • 现场调整, 监控访问量, 对自然流量激增的数据延长过期时间或设置为永久key.
  • 后台刷新数据, 启动定时任务, 高峰期来临之前, 刷新数据有效期, 确保不丢失.
  • 二级缓存, 设置不同的失效时间, 保障不会被同时淘汰就行.
  • 加锁, 分布式锁, 防止被击穿, 但是要注意也是性能瓶颈, 慎用.

总结

缓存击穿, 就是单个key高热数据过期的瞬间, 数据访问量较大, 未命中Redis后, 发起了大量对统一数据的数据库访问, 导致对数据库服务器造成压力, 应对策略应该在业务数据分析与预防方面进行, 配合运行监控测试与即时调整策略, 毕竟单个key的过期监控难度较高, 配合雪崩处理策略即可.

13.4 缓存穿透

系统平稳运行过程中, 应用服务器流量随时间增量较大, Redis服务器命中率随时间逐步降低, Redis内存平稳, CPU占用激增, 数据库服务器压力激增, 导致数据库崩溃.

问题排查: Redis中出现大面积未命中, 尤其是非正常URL访问.

问题分析:

  • 获取的数据未命中Redis缓存, 在数据库中也不存在.
  • Redis获取到null数据未进行持久化, 直接返回.
  • 下次此类数据到达重复上述过程, 直接返回null.
  • 出现黑客攻击服务器.

解决方案(术)

  • 缓存null, 对查询结果为null的数据进行缓存(长期使用, 定期清理), 设定短时限, 例如30-60s, 最高5min.

  • 白名单策略

    • 提前预热各种分类数据id对应的bitmaps, id作为bitmaps的offset, 相当于设置了数据白名单, 当加载正常数据时放行, 加载异常数据时直接拦截. (效率偏低)
    • 使用布隆过滤器 (有关布隆过滤器的命中问题对当前状况可以忽略)
  • 实施监控redis命中率, 业务正常范围时会有一个波动值, null数据的占比.

    • 非活动时段波动: 通常检测3-5倍, 超过5倍纳入重点排查对象
    • 活动时间波动: 通常检测10-50倍, 超过50倍纳入重点排查对象.

    根据倍数不同, 启动不同的排查流程. 然后使用黑名单进行防控运营.

  • key加密, 问题出现后临时启动防灾业务key, 对key进行业务层传输加密服务, 设定校验程序, 对请求的key校验.

13.5 性能指标监控

  • 性能指标: Performance

  • 内存指标: Memory

  • 基本活动指标: Basic activity

  • 持久性指标: Persistence
    准备工作:**

  • 日常例行统计数据访问记录, 统计访问频度较高的热点数据.

  • 利用LRU数据删除策略, 构建数据留存队列. (storm与kafka配合)

准备工作:

  • 将统计结果中的数据分类, 根据级别, redis优先加载级别较高的热点数据.
  • 利用分布式多服务器同时进行数据读取, 提速数据加载过程.

实时:

  • 使用脚本程序固定触发数据预热过程
  • 如果条件允许, 使用了CDN(内容分发网络), 效果会更好.

总结:

缓存预热就是系统启动前, 提前将相关的缓存数据直接加载到缓存系统. 避免在用户请求的时候, 先查询数据库, 然后再将写入缓存的问题. 用户直接查询事先被预热的缓存数据.

13.2 缓存雪崩

数据库服务器崩溃, 以下几种:

  • 系统平稳运行过程中, 忽然数据库连接量激增.
  • 应用服务器无法及时处理请求.
  • 大量408, 500错误页面出现
  • 客户反复刷新页面获取数据
  • 数据库崩溃
  • 应用服务器崩溃
  • 重启应用服务器无效
  • Redis服务器崩溃
  • Redis集群崩溃
  • 重启数据库后再次被瞬间流量放倒.

问题排查

  • 在一个较短的时间内, 缓存中较多的key集中过期.
  • 此周期内请求访问过期的数据, redis未命中, redis向数据库获取数据.
  • 数据库同时接收到大量的请求无法及时处理.
  • Redis大量请求被积压, 开始出现超时现象.
  • 数据库流量激增, 数据库崩溃.
  • 重启后仍然面对缓存中无数据可用
  • Redis服务器资源被严重占用, Redis服务器崩溃
  • Redis集群崩塌, 集群瓦解
  • 应用服务器无法及时得到数据响应请求, 来自客户端的请求数量越来越多, 应用服务器崩溃
  • 应用服务器, redis, 数据库全部重启, 效果不理想.

解决方案(道), 设计角度

  • 更多的页面静态化处理
  • 构建多级缓存架构 (Nginx缓存 + redis缓存 + ehcache缓存)
  • 检测MySql严重耗时业务进行优化, 对数据库的瓶颈进行排查.
  • 灾难预警机制, 监控redis服务器性能指标 (CPU占用, CPU使用率, 内存容量, 查询平均响应时间, 线程数)
  • 限流, 降级. 短时间范围内牺牲一些客户体验, 限制一部分请求访问, 降低应用服务器压力, 待业务低速运转后再逐步放开访问.

解决方案(术), 实施角度

  • LRU与LFU切换
  • 数据有效期策略调整
    • 根据业务数据有效期进行分类错峰, A类90min, B类80min, C类70min
    • 过期时间使用固定时间 + 随机值的形式, 稀释集中到期的key的数量.
  • 超热数据使用永久key
  • 定期维护(自动 + 人工), 对即将过期的数据做访问量分析, 确认是否延时, 配合访问量统计做热点数据的延时.
  • 加锁 , 慎用.

总结

缓存雪崩就是瞬间过期数据量太大, 导致对数据库服务器造成压力, 如果能够有效避免过期时间集中, 可以有效解决雪崩现象的出现(约40%), 配合其他策略一起使用, 并监控服务器的运行数据, 根据运行记录做快速调整.

13.3 缓存击穿(热点key)

系统平稳运行过程中, 数据库连接瞬间激增, Redis服务器无大量key过期, Redis内存平稳, Redis服务器CPU正常, 热点key过期, 数据库服务器压力激增, 导致数据库崩溃.

问题排查

  • Redis中某个key过期, 该key访问量巨大.
  • 多个数据请求从服务器直接压到Redis后, 均未命中.
  • Redis在短时间内发起了大量对数据库中同一数据的访问.

问题分析: 单个key高热数据, 短时过期.

解决方案(术)

  • 预先设定, 以电商为例, 每个商家根据店铺等级, 指定若干款主打商品, 在购物节期间, 加大此类信息key的过期时长.
  • 现场调整, 监控访问量, 对自然流量激增的数据延长过期时间或设置为永久key.
  • 后台刷新数据, 启动定时任务, 高峰期来临之前, 刷新数据有效期, 确保不丢失.
  • 二级缓存, 设置不同的失效时间, 保障不会被同时淘汰就行.
  • 加锁, 分布式锁, 防止被击穿, 但是要注意也是性能瓶颈, 慎用.

总结

缓存击穿, 就是单个key高热数据过期的瞬间, 数据访问量较大, 未命中Redis后, 发起了大量对统一数据的数据库访问, 导致对数据库服务器造成压力, 应对策略应该在业务数据分析与预防方面进行, 配合运行监控测试与即时调整策略, 毕竟单个key的过期监控难度较高, 配合雪崩处理策略即可.

13.4 缓存穿透

系统平稳运行过程中, 应用服务器流量随时间增量较大, Redis服务器命中率随时间逐步降低, Redis内存平稳, CPU占用激增, 数据库服务器压力激增, 导致数据库崩溃.

问题排查: Redis中出现大面积未命中, 尤其是非正常URL访问.

问题分析:

  • 获取的数据未命中Redis缓存, 在数据库中也不存在.
  • Redis获取到null数据未进行持久化, 直接返回.
  • 下次此类数据到达重复上述过程, 直接返回null.
  • 出现黑客攻击服务器.

解决方案(术)

  • 缓存null, 对查询结果为null的数据进行缓存(长期使用, 定期清理), 设定短时限, 例如30-60s, 最高5min.

  • 白名单策略

    • 提前预热各种分类数据id对应的bitmaps, id作为bitmaps的offset, 相当于设置了数据白名单, 当加载正常数据时放行, 加载异常数据时直接拦截. (效率偏低)
    • 使用布隆过滤器 (有关布隆过滤器的命中问题对当前状况可以忽略)
  • 实施监控redis命中率, 业务正常范围时会有一个波动值, null数据的占比.

    • 非活动时段波动: 通常检测3-5倍, 超过5倍纳入重点排查对象
    • 活动时间波动: 通常检测10-50倍, 超过50倍纳入重点排查对象.

    根据倍数不同, 启动不同的排查流程. 然后使用黑名单进行防控运营.

  • key加密, 问题出现后临时启动防灾业务key, 对key进行业务层传输加密服务, 设定校验程序, 对请求的key校验.

13.5 性能指标监控

  • 性能指标: Performance
  • 内存指标: Memory
  • 基本活动指标: Basic activity
  • 持久性指标: Persistence
  • 错误指标: Error
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐