Redis笔记

基础篇

Redis入门概述

作用

  • 缓存
    在这里插入图片描述

与传统数据库关系(mysql)
Redis是key-value数据库(NoSQL一种),mysql是关系数据库
Redis数据操作主要在内存,而mysql主要存储在磁盘
Redis在某一些场景使用中要明显优于mysql,比如计数器、排行榜等方面
Redis通常用于一些特定场景,需要与Mysql一起配合使用
两者并不是相互替换和竞争关系,而是共用和配合使用

  • 持久化

内存存储和持久化(RDB+AOF)
redis支持异步将内存中的数据写到硬盘上,同时不影响继续服务

  • 高可用架构搭配

单机
主从
哨兵
集群

  • 分布式锁
  • 队列

Reids提供list和set操作,这使得Redis能作为一个很好的消息队列平台来使用。
我们常通过Reids的队列功能做购买限制。比如到节假日或者推广期间,进行一些活动,
对用户购买行为进行限制,限制今天只能购买几次商品或者一段时间内只能购买一次。也比较适合适用。

  • 排行榜+点赞

在互联网应用中,有各种各样的排行榜,如电商网站的月度销量排行榜、社交APP的礼物排行榜、小程序的投票排行榜等等。Redis提供的zset数据类型能够快速实现这些复杂的排行榜。
比如小说网站对小说进行排名,根据排名,将排名靠前的小说推荐给用户

在这里插入图片描述

Redis安装配置

  • docker下安装
  1. docker pull redis:7.0.0
  2. docker run -p 6379:6379 --name redis --privileged=true -v /docker-v/redis/redis.conf:/etc/redis/redis.conf -v /docker-v/redis/data:/data -d redis:7.0.0 redis-server /etc/redis/redis.conf

Redis10大数据类型

Redis键(key)常用命令

keys * 查看当前库所有的key
exists key 判断某个key是否存在
type key查看key的类型
del key 删除指定的key数据
unlink key 非阻塞删除,仅仅将key从keyspace元数据中删除,真正的删除会在后续异步中操作
ttl key 查看还有多少秒过期,-1表示永不过期,-2表示已经过期
expire key 秒钟 为给定的key设置过期时间
move key dbindex[0-15] 将当前数据库的key移动到给定的数据库db中
select dbindex 切换数据库[0-15],默认为0
dbsize查看当前数据库key的数量
flushdb 清空当前库
flushall 通杀全部库

String字符串

  • set key value | get key
    set key value [NX|XX] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]
    在这里插入图片描述
    KEEPTTL选项演示
    在这里插入图片描述
  • 同时设置/获取多个键值
    mset key value [key value]
    mget key [key ...]
    mset/mget/msetnx
    mset:同时设置一个或多个 key-value 对。
    mget:获取所有(一个或多个)给定 key 的值。
    在这里插入图片描述
    msetnx:同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。
    在这里插入图片描述
  • 获取/设置指定区间范围内的值
    getrange:获取指定区间范围内的值,类似between…and的关系
    从零到负一表示全部
    在这里插入图片描述
    setrange设置指定区间范围内的值,格式是setrange key值 具体值
    在这里插入图片描述
  • 数值减增
    一定要是数字才能进行加减
    INCR key: 递增数字
    INCRBY key increment: 指定增加数字
    DECR key: 递减数字
    DECRBY key increment: 指定减少数字
    在这里插入图片描述
  • 获取字符串长度和内容追加
    STRLEN key
    APPEND key value
    在这里插入图片描述
  • 分布式锁
    setex:设置带过期时间的key,动态设置。
    setex 键 秒值 真实值
    在这里插入图片描述
    setnx:只有在 key 不存在时设置 key 的值。
    在这里插入图片描述
  • 先get再set
    getset:将给定 key 的值设为 value ,并返回 key 的旧值(old value)。
    在这里插入图片描述

List列表

一个双端链表的结构,容量是2的32次方减1个元素,大概40多亿,主要功能有push/pop等,一般用在栈、队列、消息队列等场景。
left、right都可以插入添加
如果键不存在,创建新的链表
如果键已存在,新增内容
如果值全移除,对应的键也就消失了。
它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。
在这里插入图片描述

  • lpush/rpush/lrange
    在这里插入图片描述
    在这里插入图片描述
  • lpop/rpop
    在这里插入图片描述
    在这里插入图片描述
  • lindex,通过索引获取列表中的元素
    lindex key index
    在这里插入图片描述
  • llen 获取列表中元素的个数
    在这里插入图片描述
  • lrem
    lrem key 数字n 给定值v1: 删除n个值等于v1的元素
    在这里插入图片描述
  • ltrim
    ltrim key 开始index 结束index: 截取指定索引区间的元素
    在这里插入图片描述
  • rpoplpush
    rpoplpush 源列表 目标列表: 移除列表的最后一个元素,并将该元素添加到另一个列表并返回
    在这里插入图片描述
  • lset
    lset key index value:左边指定列表索引中的数值设置值
    在这里插入图片描述
  • linsert
    linsert key before/after 已有值 插入的新值: 在list某个已有值的前后再添加具体值
    在这里插入图片描述

Hash哈希

  • hset/hget/hmset/hmget/hgetall/hdel
    在这里插入图片描述
    在这里插入图片描述
  • hlen
    hlen key: 获取key内的全部数量
    在这里插入图片描述
  • hexists
    hexists key field:判断key是否有fied字段
    在这里插入图片描述
  • hkeys/hvals
    hkeys key:查看key中所有的键
    hvals key:查看key中所有的值
    在这里插入图片描述
  • hincrby/hincrbyfloat
    hincrby key field increment:给key中field字段的值增加
    hincrbyfloat key field increment:给key中field字段的值增加(小数)
    在这里插入图片描述
  • hsetnx
    hsetnx key field value: 不存在field赋值,存在了无效。
    在这里插入图片描述
  • 应用场景

新增商品 → hset shopcar:uid1024 334488 1
新增商品 → hset shopcar:uid1024 334477 1
增加商品数量 → hincrby shopcar:uid1024 334477 1
商品总数 → hlen shopcar:uid1024
全部选择 → hgetall shopcar:uid1024
在这里插入图片描述

Set集合

单值多value,且不重复

  • sadd/smembers
    sadd key member [member ...]: 添加元素
    smembers key: 遍历集合中的所有元素
    在这里插入图片描述

  • sismember
    sismember key member:判断元素是否在集合中
    在这里插入图片描述

  • srem
    ```srem key member [member …]`:删除元素
    在这里插入图片描述

  • scard
    scard key:获取集合中元素的个数
    在这里插入图片描述

  • srandmember
    srandmember key [数字]:从集合中随机展现设置的数字个数元素,元素不删除
    在这里插入图片描述

  • spop
    spop key [数字]:从集合中随机弹出一个元素,出一个删一个
    在这里插入图片描述

  • smove
    smove key1 key2 key1中的值: 将key1里已存在的某个值赋给key2
    在这里插入图片描述

  • 集合运算

集合A:a b c 1 2
集合B:1 2 3 a x

  • 集合的差集运算A-B

属于A但不属于B的元素构成的集合
SDIFF key [key ...]
在这里插入图片描述

  • 集合的并集运算A∪B

属于A或者属于B的元素合并后的集合
SUNION key [key ...]
在这里插入图片描述

  • 集合的交集运算A∩B

同时属于A和B的元素构成的集合
SINTER key [key ...]
在这里插入图片描述
SINTERCARD numkeys key [key ...] [limit limit]
在这里插入图片描述

  • 应用场景
  • 微信抽奖小程序
    在这里插入图片描述
1 用户ID,立即参与按钮sadd key 用户ID
2 显示已经有多少人参与了,上图23208人参加SCARD key
3 抽奖(从set中任意选取N个中奖人)SRANDMEMBER key 2 随机抽奖2个人,元素不删除 SPOP key 3 随机抽奖3个人,元素会删除
  • 微信朋友圈点赞查看同赞朋友
    在这里插入图片描述
1 新增点赞sadd pub:msgID 点赞用户ID1 点赞用户ID2
2 取消点赞srem pub:msgID 点赞用户ID
3 展现所有点赞过的用户SMEMBERS pub:msgID
4 点赞用户数统计,就是常见的点赞红色数字scard pub:msgID
5 判断某个朋友是否对楼主点赞过SISMEMBER pub:msgID 用户ID
  • QQ内推可能认识的人
    在这里插入图片描述

Zset有序集合(sorted set)

  • zadd
    zadd key score member [score member ...]:添加元素和分数
    在这里插入图片描述

  • zrange
    zrange key start stop [withscores]:按照元素分数从大大小排序,返回索引从start到stop之间的所有元素
    在这里插入图片描述

  • zrevrange
    zrevrange key start stop [withscores]:按照元素分数从大到小排序
    在这里插入图片描述

  • zrangebyscore
    zrangebyscore key min key [withscores] [limit offset count]: 获取指定分数范围的元素
    (表示不包含,limit的作用是返回限制,limit 开始下标 多少步
    在这里插入图片描述

  • zscore
    zscore key member:获取指定元素的分数
    在这里插入图片描述

  • zcard
    zcard key:获取集合中元素的数量
    在这里插入图片描述

  • zrem
    zrem key member:删除值相等的元素
    在这里插入图片描述

  • zincrby
    zincrby key increment member:增加元素的分数
    在这里插入图片描述

  • zcount
    zcount key min max:获取指定分数范围内元素的个数
    在这里插入图片描述

  • zmpop
    从键名列表中的第一个非空排序集中弹出一个或多个元素,它们是成员分数对
    在这里插入图片描述

  • zrank key values:获取下标值

  • zrevrank key values: 逆序获取下标值
    在这里插入图片描述

  • 应用场景

定义商品销售排行榜(sorted set集合),key为goods:sellsort,分数为商品销售数量

商品编号1001的销量是9,商品编号1002的销量是15zadd goods:sellsort 9 1001 15 1002
有一个客户又买了2件商品1001,商品编号1001销量加2zincrby goods:sellsort 2 1001
求商品销量前10名ZRANGE goods:sellsort 0 9 withscores

在这里插入图片描述

bitmap位图

  • setbit
    setbit key offset value
    Bitmap的偏移量是从零开始算的

  • getbit
    getbit offset value
    在这里插入图片描述

  • strlen
    strlen key: 统计多少个字节数
    不是字符串长度而是占据几个字节,超过8位后自己按照8位一组一byte再扩容
    在这里插入图片描述

  • bitcount
    全部键里面含有1的有多少个?
    在这里插入图片描述

  • bitop
    连续2天都签到的用户
    加入某个网站或者系统,它的用户有1000W,做个用户id和位置的映射
    比如0号位对应用户id:u1
    比如1号位对应用户id:u2
    在这里插入图片描述

HyperLogLog基数统计

去重复统计功能的基数估计算法-就是HyperLogLog
基数:是一种数据集,去重复后的真实个数
基数统计:用于统计一个集合中不重复的元素个数,就是对集合去重复后剩余元素的计算
在这里插入图片描述
在这里插入图片描述

GEO地理空间

在这里插入图片描述

  • geoadd
    GEOADD多个经度(longitude)、纬度(latitude)、位置名称(member)添加到指定的 key 中
    在这里插入图片描述

  • geopos
    从键里面返回所有给定位置元素的位置(经度和纬度)
    在这里插入图片描述

  • geohash
    用于获取—个或多个位置元素的geohash 值。
    在这里插入图片描述

  • geodist
    两个位置之间的距离
    在这里插入图片描述

  • georadius
    以给定的经纬度为中心,返回键包含的位置元素当中,与中心的距离不超过给定最大距离的所有位置元素。
    GEORADIUS city 116.418017 39.914402 10 km withdist withcoord withhash count 10 desc
    WITHDIST: 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。 距离的单位和用户给定的范围单位保持一致。
    WITHCOORD: 将位置元素的经度和维度也一并返回。
    WITHHASH: 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大
    COUNT 限定返回的记录数。
    在这里插入图片描述

  • georadiusbymember

  • 在这里插入图片描述

Stream流

在这里插入图片描述
一个消息链表,将所有加入的消息都串起来,每个消息都有一个唯一的 ID 和对应的内容

Message Content消息内容
Consumer group消费组,通过XGROUP CREATE 命令创建,同一个消费组可以有多个消费者
Last_delivered_id游标,每个消费组会有个游标 last_delivered_id,任意一个消费者读取了消息都会使游标 last_delivered_id 往前移动。
Consumer消费者,消费组中的消费者
Pending_ids消费者会有一个状态变量,用于记录被当前消费已读取但未ack的消息Id,如果客户端没有ack,这个变量里面的消息ID会越来越多,一旦某个消息被ack它就开始减少。这个pending_ids变量在Redis官方被称之为 PEL(Pending Entries List),记录了当前已经被客户端读取的消息,但是还没有 ack (Acknowledge character:确认字符),它用来确保客户端至少消费了消息一次,而不会在网络传输的中途丢失了没处理
基本命令理论简介
  • 队列相关指令
    在这里插入图片描述
  • 消费组相关指令
    在这里插入图片描述
  • 四个特殊符号
  • - +: 最小和最大可能出现的id
  • $ 表示只消费新的消息,当前流中最大的id,可以用于将要到来的消息
  • > 用于XREADGROUP命令,表示迄今还没有发送给组中使用者的信息,会更新消费者组的最后ID
  • * 用于XADD命令中,让系统自动生成id
基本指令实操
队列相关指令
  • xadd
    XADD 用于向Stream 队列中添加消息,如果指定的Stream 队列不存在,则该命令执行时会新建一个Stream 队列
    消息id必须比上一个大,默认使用*表示自动生成id
    在这里插入图片描述
    Redis对于ID有强制要求,格式必须是时间戳-自增Id这样的方式,且后续ID不能小于前一个ID

  • xrange
    xrange key start end [Count count]: 用于获取消息列表(可以指定范围),忽略删除的消息
    start表示开始值,-代表最小值
    end表示结束值,+代表最大值
    在这里插入图片描述

  • xrevrange
    xrange是相反的

  • 在这里插入图片描述

  • xdel
    xdel key id [id ...]: 删除一个或多个消息
    在这里插入图片描述

  • xlen
    用于获取Stream队列的消息的长度
    在这里插入图片描述

  • xtrim
    用于对Stream的长度进行截取,如超长会进行截取
    maxlen:允许的最大长度,对流进行修剪限制长度
    minid:允许的最小id,从某个id值开始比该id值小的将会被抛弃
    在这里插入图片描述
    在这里插入图片描述

  • xread
    用于获取消息(阻塞/非阻塞),只会返回大于指定lD的消息
    $代表特殊ID,表示以当前Stream已经存储的最大的ID作为最后一个ID,当前Stream中不存在大于当前最大ID的消息,因此此时返回nil
    0-0代表从最小的ID开始获取Stream中的消息,当不指定count,将会返回Stream中的所有消息,注意也可以使用0(00/000也都是可以的……)
    在这里插入图片描述
    阻塞:
    请redis-cli启动第2个客户端连接上来
    在这里插入图片描述

消费组相关指令
  • xgroup create
    用于创建用户组
    $表示从Stream尾部开始消费
    0表示从Stream头部开始消费
    在这里插入图片描述

  • xreadgroup group
    >,表示从第一条尚未被消费的消息开始读取
    消费组groupA内的消费者consumer1从mystream消息队列中读取所有消息
    在这里插入图片描述
    但是,不同消费组的消费者可以消费同一条消息
    在这里插入图片描述
    消费组的目的是让组内的多个消费者共同分担读取消息,所以,我们通常会让每个消费者读取部分消息,从而实现消息读取负载在多个消费者间是均衡分布的
    在这里插入图片描述

  • 重点问题

1基于 Stream 实现的消息队列,如何保证消费者在发生故障或宕机再次重启后,仍然可以读取未处理完的消息?
2Streams 会自动使用内部队列(也称为 PENDING List)留存消费组里每个消费者读取的消息保底措施,直到消费者使用 XACK 命令通知 Streams“消息已经处理完成”。
3消费确认增加了消息的可靠性,一般在业务处理完成之后,需要执行 XACK 命令确认消息已经被消费完成

在这里插入图片描述

  • xpending
    查询每个消费组内所有消费者「已读取、但尚未确认」的消息
    在这里插入图片描述
    查看某个消费者具体读取了哪些数据
    在这里插入图片描述
    consumer2已读取的消息的 ID是1659430293537-0
    一旦消息1659430293537-0被consumer2处理了consumer2就可以使用 XACK 命令通知 Streams,然后这条消息就会被删除

  • xack
    向消息队列确认消息处理已完成
    在这里插入图片描述
    在这里插入图片描述

XINFO用于打印Stream\Consumer\Group的详细信息

在这里插入图片描述

bitfield位域

Redis持久化

在这里插入图片描述

RDB(Redis DataBase)

简介

实现类似照片记录效果的方式,就是把某一时刻的数据和状态以文件的形式写到磁盘上,也就是快照。这样一来即使故障宕机,快照文件也不会丢失,数据的可靠性也就得到了保证这个快照文件就称为RDB文件(dump.rdb),其中,RDB就是Redis DataBase的缩写
在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot内存快照,它恢复时再将硬盘快照文件直接读回到内存里

案例演示
  • 需求说明
    在这里插入图片描述
  • 配置文件(6 VS 7)
    7之前:
    在这里插入图片描述
    在这里插入图片描述
    7之后:
    在这里插入图片描述
  • 操作步骤

自动触发

  1. 按照redis.conf里配置的save

  2. 本次案例5秒2次修改
    在这里插入图片描述

  3. 修改dump文件保存路径
    在这里插入图片描述

  4. 修改dump文件名称
    在这里插入图片描述

  5. 触发备份
    在这里插入图片描述

  6. 如何恢复
    将备份文件(dump.rdb)移动到redis安装目录并启动服务即可
    备份成功后故意用flushdb清空redis,看看是否可以恢复数据
    在这里插入图片描述物理恢复,一定服务和备份分机隔离,以防生产机物理损坏后备份文件也挂了。
    在这里插入图片描述

手动触发

Redis提供了两个命令来生成RDB文件分别是save和bgsave
Save会阻塞当前redis服务器,直到持久化完成才能处理其他命令,线上禁止使用
在这里插入图片描述
bgsave(默认)会在后台异步进行快照操作不阻塞同时可以处理其他命令,该触发方式会fork一个子进程由子进程进行持久化工作
在这里插入图片描述
lastsave获取最后一次成功执行快照的时间
在这里插入图片描述


  • 优势

适合大规模的数据恢复
按照业务定时备份
对数据完整性和一致性要求不高
RDB文件在内存中的加载速度要比AOF快得多

  • 劣势
  • 在一定间隔时间做一次备份,所以如果redis意外down掉的话,就会丢失从当前至最近一次快照期间的数据,快照之间的数据会丢失
  • 内存数据的全量同步,如果数据量太大会导致I/0严重影响服务器性能
  • RDB依赖于主进程的fork,在更大的数据集中,这可能会导致服务请求的瞬间延迟。fork的时候内存中的数据被克降了一份,大致2倍的膨胀性,需要考虑

数据丢失案例

  1. 正常录入数据
    在这里插入图片描述
  2. 模拟redis服务down掉停止redis服务
  3. redis重新启动查看清空
    在这里插入图片描述
  • 检测修复dump.rdb文件
    在这里插入图片描述
  • 触发RDB快照情况

配置文件中默认的快照配置
手动save/bgsave命令
执行flushall/flushdb命令也会产生dump.rdb文件,但里面是空的,无意义
执行shutdown且没有设置开启AOF持久化
主从复制时,主节点自动触发

  • 禁止快照文件

动态所有停止RDB保存规则的方法: redis-cli config set save “”
配置文件禁止
在这里插入图片描述

  • RDB优化配置详解(配置文件SNAPSHOTTING模块)

save <seconds> <changes>:设置自动生成RDB快照文件规则
dbfilename:rdb文件名字
dir:rdb文件存放位置
stop-writes-on-bgsave-error

默认yes
如果配置成no,表示你不在乎数据不一致或者有其他的手段发现和控制这种不一致,那么在快照写入失败时,
也能确保redis继续接受新的写请求

rdbcompression:

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

rdbchecksum

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

rdb-del-sync-files

在没有持久性的情况下删除复制中使用的RDB文件启用。默认情况下no,此选项是禁用的。

在这里插入图片描述

AOF(Append Only File)

简介

以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作
默认情况下,redis是没有开启AOF(append only file)的。开启AOF功能需要设置配置: appendonly yes
aof保存的是appendonly.aof文件

工作流程

在这里插入图片描述

1Client作为命令的来源,会有多个源头以及源源不断的请求命令
2在这些命令到达Redis Server 以后并不是直接写入AOF文件,会将其这些命令先放入AOF缓存中进行保存。这里的AOF缓冲区实际上是内存中的一片区域,存在的目的是当这些命令达到一定量以后再写入磁盘,避免频繁的磁盘IO操作。
3OF缓冲会根据AOF缓冲区同步文件的三种写回策略将命令写入磁盘上的AOF文件
4随着写入AOF内容的增加为避免文件膨胀,会根据规则进行命令的合并(又称AOF重写),从而起到AOF文件压缩的目的。
5当Redis Server 服务器重启的时候会从AOF文件载入数据。
  • 三种写回策略

在这里插入图片描述
always:同步写回,每个写命令执行完立刻同步地将日志写回磁盘
everysec:每秒写回,每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,每隔1秒把缓冲区中的内容写入磁盘
no:操作系统控制的写回,每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
在这里插入图片描述

案例演示和说明
配置文件说明(6 VS 7)
  • 如何开启aof
    在这里插入图片描述
  • 使用默认写回策略,每秒钟
    在这里插入图片描述
  • aof文件-保存路径
  • redis6
    AOF保存文件的位置和RDB保存文件的位置—样,都是通过redis.conf配置文件的dir配置
  • redis7
    在这里插入图片描述
    dir + appenddirname
  • aof文件-保存名称
  • redis6
    有且只有一个aof文件
  • redis7

Redis7.0 Multi Part AOF的设计``
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

正常恢复
  1. 启动:设置Yes,修改默认的appendonly no,改为yes

  2. 写操作接续,生成aof文件到指定的目录
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

  3. 重启redis服务器进行测试

异常恢复
  1. 故意乱写正常的AOF文件,模拟网络闪断文件写error
    在这里插入图片描述
  2. 重启Redis之后就会进行AOF文件的载入,发现启动都不行
  3. 异常修复命令: redis-check-aof --fix进行修复
    在这里插入图片描述
优劣势
  • 优势

更好的保护数据不丢失、性能高、可做紧急恢复

  • 劣势

相同数据集的数据而言aof文件要远大于rdb文件,恢复速度慢于rdb
aof运行效率要慢于rdb,每秒同步策略效率较好,不同步效率和rdb相同

aof重写机制

启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集。

  • 触发机制
  • 自动触发
    满足配置文件中的选项后,Redis会记录上次重写时的AOF大小,
    默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时
  • 手动触发
    客户端向服务器发送bgrewriteaof命令
  • 案例说明

在这里插入图片描述

  1. 前期配置准备
    开启aof
    在这里插入图片描述
    重写峰值修改为1k
    在这里插入图片描述
    关闭混合,设置为no
    在这里插入图片描述
    删除之前的全部aof和rdb,清除干扰项
  2. 自动触发案例01
    完成上述正确配置,重启redis服务器,执行set k1 v1查看aof文件是否正常
    在这里插入图片描述
    查看三大配置文件
    在这里插入图片描述
    在这里插入图片描述
    k1不停1111111暴涨
    在这里插入图片描述
    重写触发
    在这里插入图片描述
  3. 自动触发案例02
    客户端向服务器发送bgrewriteaof命令
    在这里插入图片描述
  4. 结论
    在这里插入图片描述
  • 重写原理
    在这里插入图片描述

RDB-AOF混合持久化

在这里插入图片描述

RDB持久化方式能够在指定的时间间隔能对你的数据进行快照存储
AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操作到文件末尾.

  • 同时开启两种持久化方式

在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整.
RDB的数据不实时,同时使用两者时服务器重启也只会找AOF文件。那要不要只使用AOF呢?作者建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份),留着rdb作为一个万一的手段。

  • rdb+aof混合方式

结合了RDB和AOF的优点,既能快速加载又能避免丢失过多的数据。

  1. 开启混合方式设置
    设置aof-use-rdb-preamble的值为yes yes表示开启,设置为no表示禁用
  2. RDB+AOF的混合方式—>结论:RDB镜像做全量持久化,AOF做增量持久化
    先使用RDB进行快照存储,然后使用AOF持久化记录所有的写操作,当重写策略满足或手动触发重写的时候,将最新的数据存储为新的RDB记录。这样的话,重启服务的时候会从RDB和AOF两部分恢复数据,既保证了数据完整性,又提高了恢复数据的性能。简单来说:混合持久化方式产生的文件一部分是RDB格式,一部分是AOF格式。—》AOF包括了RDB头部+AOF混写
    在这里插入图片描述

纯缓存模式

同时关闭rdb+aof

  • rdb

save “”
禁用rdb持久化模式下,我们仍然可以使用命令save、bgsave生成rdb文件

  • aof

appendonly no
禁用aof持久化模式下,我们仍然可以使用命令bgrewriteaof生成aof文件

Redis事务

简介

可以一次执行多个命令,本质是一组命令的集合。一个事务中的所有命令都会序列化,
按顺序地串行化执行而不会被其它命令插入,不许加塞
一个队列中,一次性、顺序性、排他性的执行一系列命令

Redis事务 vs 数据库事务

1 单独的隔离操作Redis的事务仅仅是保证事务里的操作会被连续独占的执行,redis命令执行是单线程架构,在执行完事务内所有指令前是不可能再去同时执行其他客户端的请求的
2 没有隔离级别的概念因为事务提交前任何指令都不会被实际执行,也就不存在”事务内的查询要看到事务里的更新,在事务外查询不能看到”这种问题了
3不保证原子性Redis的事务不保证原子性,也就是不保证所有指令同时成功或同时失败,只有决定是否开始执行全部指令的能力,没有执行到一半进行回滚的能力
4 排它性Redis会保证一个事务内的命令依次执行,而不会被其它命令插入

实操

  • 常用命令
    在这里插入图片描述

  • case1 正常执行
    在这里插入图片描述

  • case2 放弃事务
    在这里插入图片描述

  • case3 全体连坐(一个语法错误,全部出错)
    在这里插入图片描述

  • case4 冤有头债有主(前面语法都没有问题,编译通过,执行exec后报错,哪条语句出错哪条不执行类似于RuntimeException)
    在这里插入图片描述

  • case5 watch监控

Redis使用Watch来提供乐观锁定,类似于CAS(Check-and-Set)

  • 初始化k1和balance两个key,先监控再开启multi ,保证两key变动在同一个事务内
    在这里插入图片描述
  • 加塞篡改
    watch命令是一种乐观锁的实现,Redis在修改的时候会检测数据是否被更改,如果更改了,则执行失败
    第一个窗口蓝色框第5步执行结果返回为空,也就是相当于是失败
    在这里插入图片描述
  • unwatch
    在这里插入图片描述

总结

开启:以multi开启一个事务
入队:将多个命令入队到事务中,接到这些命令并不会立即执行,而是放到等待执行的事务队列里面
执行:由EXEC命令触发事务

Redis管道

简介

Redis是一种基于客户端-服务端模型以及请求/响应协议的TCP服务。一个请求会遵循以下步骤:
1 客户端向服务端发送命令分四步(发送命令→命令排队→命令执行→返回结果),并监听Socket返回,通常以阻塞模式等待服务端响应。
2 服务端处理命令,并将结果返回给客户端。
上述两步称为:Round Trip Time(简称RTT,数据包往返于两端的时间)
在这里插入图片描述
如果同时需要执行大量的命令,那么就要等待上一条命令应答后再执行,这中间不仅仅多了RTT(Round Time Trip),而且还频繁调用系统IO,发送网络请求,同时需要redis调用多次read()和write()系统方法,系统方法会将数据从用户态转移到内核态,这样就会对进程上下文有比较大的影响了,性能不太好
管道(pipeline)可以一次性发送多条命令给服务端,服务端依次处理完完毕后,通过一条响应一次性将结果返回,通过减少客户端与redis的通信次数来实现降低往返延时时间。pipeline实现的原理是队列,先进先出特性就保证数据的顺序性。
在这里插入图片描述
批处理命令变种优化措施,类似Redis的原生批命令(mget和mset)

案例演示

在这里插入图片描述

总结

  • Pipeline与原生批量命令对比

原生批量命令是原子性(例如:mset, mget), pipeline是非原子性
原生批量命令一次只能执行一种命令,pipeline支持批量执行不同命令
原生批命令是服务端实现,而pipeline需要服务端与客户端共同完成

  • Pipeline与事务对比

事务具有原子性,管道不具有原子性
管道一次性将多条命令发送到服务器,事务是一条一条的发,事务只有在接收到exec命令后才会执行,管道不会
执行事务时会阻塞其他命令的执行,而执行管道中的命令时不会

  • 使用Pipeline注意事项

pipeline缓冲的指令只是会依次执行,不保证原子性,如果执行中指令发生异常,将会继续执行后续的指令
使用pipeline组装的命令个数不能太多,不然数据量过大客户端阻塞的时间可能过久,同时服务端此时也被迫回复一个队列答复,占用很多内存

Redis复制(relica)

简介

就是主从复制,master以写为主,Slave以读为主
当master数据变化的时候,自动将新的数据异步同步到其它slave数据库
作用:

  • 读写分离
  • 数据备份
  • 容灾恢复
  • 水平扩容支撑高并发

操作:

  • 配从(库)不配主(库)
  • 权限细节

master如果配置了requirepass参数,需要密码登陆
那么slave就要配置masterauth来设置校验密码,否则的话master会拒绝slave的访问请求
在这里插入图片描述

  • 基本操作命令
  • info replication: 查看复制节点的主从关系和配置信息
  • replicaof 主库ip port
    一般配置在redis配置文件中
  • slaveof 主库ip port
    每次与master断开之后,都需要重新连接,除非你配置进redis.conf文件
    在运行期间修改slave节点的信息,如果该数据库已经是某个主数据库的从数据库,那么会停止和原主数据库的同步关系转而和新的主数据库同步,重新拜码头
  • slaveof no one: 使当前数据库停止与其他数据库的同步,转成主数据库,自立为王

案例演示

跳转查看

架构说明

在这里插入图片描述

一个master和两个slave

  • 主从问题演示
  1. 从机可以执行写命令吗
    在这里插入图片描述

  2. 从机切入点问题

  3. 主机shutdown后,从机有机会上位吗
    从机不动,原地待命,从机数据可以正常使用;等待主机重启动归来
    在这里插入图片描述

  4. 主机shutdown后,重启后主从关系还在吗?从机还能否顺利复制?
    在这里插入图片描述
    在这里插入图片描述
    依旧在

  5. 某台从机down后,master继续,从机重启后它能跟上大部队吗?
    可以

  • 命令操作手动指定

从机停机去掉配置文件中的配置项,3台目前都是主机状态,各不从属
在这里插入图片描述
3台master
在这里插入图片描述
预设的从机上执行命令
在这里插入图片描述
两台从机重新启动,配置清空
在这里插入图片描述

  • 配置 vs 命令的区别

配置:持久稳定
命令:档次生效

薪火相传

上一个slave可以是下一个slave的master,slave同样可以接收其他slaves的连接和同步请求,那么该slave作为了链条中下一个的master,可以有效减轻主master的写压力
中途变更转向:会清除之前的数据,重新建立拷贝最新的
slaveof 新主库IP新主库端口

反客为主

slaveof no one
使当前数据库停止与其他数据库的同步,转成主数据库

复制原理和工作流程

  • slave启动,同步初请

slave启动成功连接到master后会发送一个sync命令
slave首次全新连接master,一次完全同步(全量复制)将被自动执行,slave自身原有数据会被master数据覆盖清除

  • 首次连接,全量复制

master节点收到sync命令后会开始在后台保存快照(即RDB持久化,主从复制时会触发RDB),同时收集所有接收到的用于修改数据集命令缓存起来,master节点执行RDB持久化完后,master将rdb快照文件和所有缓存的命令发送到所有slave,以完成一次完全同步
而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中,从而完成复制初始化

  • 心跳持续,保持通信

repl-ping-replica-period 10
master发出PING包的周期,默认是10秒
在这里插入图片描述

  • 进入平稳,增量复制

Master继续将新的所有收集到的修改命令自动依次传给slave,完成同步

  • 从机下线,重连续传

master会检查backlog里面的offset,master和slave都会保存一个复制的offset还有一个masterId,offset是保存在backloq中的。Master只会把已经复制的offset后面的数据复制给Slave,类似断点续传

复制的缺点

由于所有的写操作都是先在Master上操作,然后同步更新到Slave上,所以从Master同步到Slave机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,Slave机器数量的增加也会使这个问题更加严重。
在这里插入图片描述
如果master挂了,默认情况下不会再slave节点中自动重选一个master

Redis哨兵(sentinel)

简介

是什么

吹哨人巡查监控后台master主机是否故障,如果故障了根据投票数自动将某一个从库转换为新主库,继续对外服务
作用: 俗称,无人值守运维

能干嘛

主从监控: 监控主从redis库运行是否正常
消息通知: 哨兵可以将故障转移的结果发送给客户端
故障转移: 如果Master异常,则会进行主从切换,将其中一个Slave作为新的Master
配置中心: 客户端通过连接哨兵来获得当前Redis服务的主节点地址

案例实战

跳转查看

架构说明

在这里插入图片描述

之前的一主两从复制,再加入三个哨兵,不存放数据,只是吹哨人

案例步骤

配置文件重点参数说明

  • bind: 服务监听地址,用于客户端连接,默认本机地址
  • daemonize: 是否以后台daemon方式运行(设置为no,否则会和docker run的-d参数冲突运行不起来)
  • protected-mode: 安全保护模式
  • port: 哨兵sentinel实例运行的端口 默认26379
  • logfile: 日志文件路径
  • pidfile: pid文件路径
  • dir: 工作目录
  • sentinel monitor <master-name> <ip> <redis-port> <quorum>:
    哨兵sentinel监控的redis主节点的 ip port
    quorum(/ˈkwɔːrəm/ 法定人数)表示最少有几个哨兵认可客观下线,同意故障迁移的法定票数
    我们知道,网络是不可靠的,有时候一个sentinel会因为网络堵塞而误以为一个master redis已经死掉了,在sentinel集群环境下需要多个sentinel互相沟通来确认某个master是否真的死了,quorum这个参数是进行客观下线的一个依据,意思是至少有quorum个sentinel认为这个master有故障,才会对这个master进行下线以及故障转移。因为有的时候,某个sentinel节点可能因为自身网络原因,导致无法连接master,而此时master并没有出现故障,所以,这就需要多个sentinel都一致认为该master有问题,才可以进行下一步操作,这就保证了公平性和高可用。
  • sentinel auth-pass <master-name> <password>: master设置了密码,连接master服务的密码
模拟master宕机
  • 手动关闭6379服务器,模拟master挂了
    在这里插入图片描述
  • 查看6380,6381状态
    在这里插入图片描述
    在这里插入图片描述

6380成为了新的master而6381还是slave,只不过换了个老大6380(6379变6380)

  • 查看sentinel日志
    在这里插入图片描述
  • 重启6379
    在这里插入图片描述

降级为slave

哨兵运行流程和选举原理

当一个主从配置中的master失效之后,sentinel可以选举出一个新的master
用于自动接替原master的工作,主从配置中的其他redis服务器自动指向新的master同步数据。一般建议sentinel采取奇数台,防止某一台sentinel无法连接到master导致误切换

运行流程,故障切换
  • SDown主观下线(Subjectively Down)

所谓主观下线(SubjectvelyDown,简称SDown)指的是单个Sentinel实例对服务器做出的下线判断,即单个sentinei认为某个服务下线(有可能是接收不到订阅,之间的网络不通等等原因)。主观下线就是说如果服务器在[sentinel down-after-milliseconds]给定的毫秒数之内没有回应PING命令或者返回一个错误消息,那么这个Sentinel会主观的(单方面的)认为这个master不可以用了。
在这里插入图片描述
sentinel down-after-milliseconds <masterName> <timeout>
表示master被当前sentinel实例认定为失效的间隔时间,这个配置其实就是进行主观下线的一个依据
master在多长时间内一直没有给Sentine返回有效信息,则认定该master主观下线。也就是说如果多久没联系上redis-servevr,认为这个redis-server进入到失效(SDOWN)状态。

  • ODown客观下线(Objectively Down)

四个参数含义:
masterName是对某个master+slave组合的一个区分标识(一套sentinel可以监听多组master+slave这样的组合)
在这里插入图片描述
quorum这个参数是进行客观下线的一个依据,法定人数/法定票数
意思是至少有quorum个sentinel认为这个master有故障才会对这个master进行下线以及故障转移。因为有的时候,某个sentinel节点可能因为自身网络原因导致无法连接master,而此时master并没有出现故障,所以这就需要多个sentinel都一致认为该master有问题,才可以进行下一步操作,这就保证了公平性和高可用

  • 选举出领导者哨兵(哨兵中选出一个领导者)

当主节点被判断客观下线以后,各个哨兵节点会进行协商,先选举出一个领导者哨兵节点(兵王)并由该领导者节点,也即被选举出的兵王进行failover(故障迁移)
哨兵领导者选出算法(Raft算法)
在这里插入图片描述
监视该主节点的所有哨兵都有可能被选为领导者,选举使用的算法是Raft算法;Raft算法的基本思路是先到先得
即在一轮选举中,哨兵A向B发送成为领导者的申请,如果B没有同意过其他哨兵,则会同意A成为领导者

  • 由领导者开始推动故障切换流程并选出一个新的master
  1. 新的master上位
    某个slave被选中成为master
    选出新master的规则,剩余slave节点健康前提下
    在这里插入图片描述
    redis.conf文件中,优先级slave-priority或者replica-priority最高的从节点(数字越小优先级越高)
    在这里插入图片描述
    复制偏移位置offset最大的从节点
    最小Run Id的从节点
  2. 其余slave认新的master为大哥
    执行slaveof no one命令让选出来的从节点成为新的主节点,并通过slaveof命令让其他节点成为其从节点
    Sentinel leader会对选举出的新master执行slaveof no one操作,将其提升为master节点
    Sentinel leader向其它slave发送命令,让剩余的slave成为新的master节点的slave
  3. 之前挂掉的master认新的master为大哥
    将之前已下线的老master设置为新选出的新master的从节点,当老master重新上线后,它会成为新master的从节点Sentinel leader会让原来的master降级为slave并恢复正常工作。
  4. 总结
    上述的failover操作均由sentinel自己独自完成,完全无需人工干预。

哨兵使用建议

哨兵节点的数量应为多个,哨兵本身应该集群,保证高可用
哨兵节点的数量应该是奇数
各个哨兵节点的配置应一致
如果哨兵节点部署在Docker等容器里面,尤其要注意端口的正确映射
哨兵集群+主从复制,并不能保证数据零丢失

Redis集群(cluster)

简介

是什么

由于数据量过大,单个Master复制集难以承担,因此需要对多个复制集进行集群,形成水平扩展每个复制集只负责存储整个数据集的一部分,这就是Redis的集群,其作用是提供在多个Redis节点间共享数据的程序集。
在这里插入图片描述
Redis集群是一个提供在多个Redis节点间共享数据的程序集
Redis集群可以支持多个Master

能干吗
  • Redis集群支持多个Master,每个Master又可以挂载多个Slave

读写分离
支持数据的高可用
支持海量数据的读写存储操作

  • 由于Cluster自带Sentinel的故障转移机制,内置了高可用的支持,无需再去使用哨兵功能
  • 客户端与Redis的节点连接,不再需要连接集群中所有的节点,只需要任意连接集群中的一个可用节点即可
  • 槽位slot负责分配到各个物理服务节点,由对应的集群来负责维护节点、插槽和数据之间的关系

集群算法-分片-槽位slot

在这里插入图片描述

redis集群的槽位slot

在这里插入图片描述
在这里插入图片描述

redis集群的分片
分片是什么使用Redis集群时我们会将存储的数据分散到多台redis机器上,这称为分片。简言之,集群中的每个Redis实例都被认为是整个数据的一个分片。
如何找到给定key的分片为了找到给定key的分片,我们对key进行CRC16(key)算法处理并通过对总分片数量取模。然后,使用确定性哈希函数,这意味着给定的key将多次始终映射到同一个分片,我们可以推断将来读取特定key的位置。

在这里插入图片描述

优势

方便扩缩容和数据分派查找
在这里插入图片描述
在这里插入图片描述

slot槽位映射,三种解决方案
  • 哈希取余分区

在这里插入图片描述

2亿条记录就是2亿个k,v,我们单机不行必须要分布式多机,假设有3台机器构成一个集群,用户每次读写操作都是根据公式:hash(key) % N个机器台数,计算出哈希值,用来决定数据映射到哪一个节点上
优点:简单粗暴,直接有效,只需要预估好数据规划好节点,例如3台、8台、10台,就能保证一段时间的数据支撑。使用Hash算法让固定的一部分请求落到同一台服务器上,这样每台服务器固定处理一部分请求(并维护这些请求的信息),起到负载均衡+分而治之的作用。
缺点:原来规划好的节点,进行扩容或者缩容就比较麻烦了额,不管扩缩,每次数据变动导致节点有变动,映射关系需要重新进行计算,在服务器个数固定不变时没有问题,如果需要弹性扩容或故障停机的情况下,原来的取模公式就会发生变化:Hash(key)/3会变成Hash(key) /?。此时地址经过取余运算的结果将发生很大变化,根据公式获取的服务器也会变得不可控。某个redis机器宕机了,由于台数数量变化,会导致hash取余全部数据重新洗牌。
  • 一致性哈希算法分区

一致性哈希算法在1997年由麻省理工学院中提出的,设计目标是为了解决分布式缓存数据变动和映射问题,某个机器宕机了,分母数量改变了,自然取余数不OK了。
提出一致性Hash解决方案。目的是当服务器个数发生变动时,尽量减少影响客户端到服务器的映射关系
三大步骤:

  1. 算法构建一致性哈希环
    一致性哈希算法必然有个hash函数并按照算法产生hash值,这个算法的所有可能哈希值会构成一个全量集,这个集合可以成为一个hash空间[0,232-1],这个是一个线性空间,但是在算法中,我们通过适当的逻辑控制将它首尾相连(0 = 232),这样让它逻辑上形成了一个环形空间。
    它也是按照使用取模的方法,前面笔记介绍的节点取模法是对节点(服务器)的数量进行取模。而一致性Hash算法是对232取模,简单来说,一致性Hash算法将整个哈希值空间组织成一个虚拟的圆环,如假设某哈希函数H的值空间为0-232-1即哈希值是一个32位无符号整形),整个哈希环如下图:整个空间按顺时针方向组织,圆环的正上方的点代表0,0点右侧的第一个点代表1,以此类推,2、3、4、……直到232-1,也就是说0点左侧的第一个点代表232-1, 0和232-1在零点中方向重合,我们把这个由232个点组成的圆环称为Hash环。
    在这里插入图片描述
  2. redis服务器ip节点映射
    将集群中各个IP节点映射到环上的某一个位置。
    将各个服务器使用Hash进行一个哈希,具体可以选择服务器的IP或主机名作为关键字进行哈希,这样每台机器就能确定其在哈希环上的位置。假如4个节点NodeA、B、C、D,经过IP地址的哈希函数计算(hash(ip)),使用IP地址哈希后在环空间的位置如下:
    在这里插入图片描述
  3. key落到服务器的落键规则
    当我们需要存储一个kv键值对时,首先计算key的hash值,hash(key),将这个key使用相同的函数Hash计算出哈希值并确定此数据在环上的位置,从此位置沿环顺时针“行走”,第一台遇到的服务器就是其应该定位到的服务器,并将该键值对存储在该节点上。
    如我们有Object A、Object B、Object C、Object D四个数据对象,经过哈希计算后,在环空间上的位置如下:根据一致性Hash算法,数据A会被定为到Node A上,B被定为到Node B上,C被定为到Node C上,D被定为到Node D上。
    在这里插入图片描述

优点:

  • 容错性
    假设Node C宕机,可以看到此时对象A、B、D不会受到影响。一般的,在一致性Hash算法中,如果一台服务器不可用,则受影响的数据仅仅是此服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它不会受到影响。简单说,就是C挂了,受到影响的只是B、C之间的数据且这些数据会转移到D进行存储
    在这里插入图片描述
  • 扩展性
    数据量增加了,需要增加一台节点NodeX,X的位置在A和B之间,那收到影响的也就是A到X之间的数据,重新把A到X的数据录入到X上即可,不会导致hash取余全部数据重新洗牌。
    在这里插入图片描述

缺点(Hash环的数据倾斜问题):
一致性Hash算法在服务节点太少时,容易因为节点分布不均匀而造成数据倾斜(被缓存的对象大部分集中缓存在某一台服务器上)问题,例如系统中只有两台服务器:
在这里插入图片描述
总结:
为了在节点数目发生改变时尽可能少的迁移数据
将所有的存储节点排列在收尾相接的Hash环上,每个key在计算Hash后会顺时针找到临近的存储节点存放。
而当有节点加入或退出时仅影响该节点在Hash环上顺时针相邻的后续节点

  • 哈希槽分区
  • 为什么出现
    在这里插入图片描述
    哈希槽实质就是一个数组,数组[0,214 -1]形成hash slot空间。
  • 能干什么
    解决均匀分配的问题,在数据和节点之间又加入了一层,把这层称为哈希槽(slot),用于管理数据和节点之间的关系,现在就相当于节点上放的是槽,槽里放的是数据。
    在这里插入图片描述
    槽解决的是粒度问题,相当于把粒度变大了,这样便于数据移动。哈希解决的是映射问题,使用key的哈希值来计算所在的槽,便于数据分配
  • 多少个hash槽
    一个集群只能有16384个槽,编号0-16383(0-2^14-1)。这些槽会分配给集群中的所有主节点,分配策略没有要求。
    集群会记录节点和槽的对应关系,解决了节点和槽的关系后,接下来就需要对key求哈希值,然后对16384取模,余数是几key就落入对应的槽里。HASH_SLOT = CRC16(key) mod 16384。以槽为单位移动数据,因为槽的数目是固定的,处理起来比较容易,这样数据移动问题就解决了。
  • 哈希槽计算
    Redis 集群中内置了 16384 个哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点。当需要在 Redis 集群中放置一个 key-value时,redis先对key使用crc16算法算出一个结果然后用结果对16384求余数[ CRC16(key) % 16384],这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,也就是映射到某个节点上。如下代码,key之A 、B在Node2, key之C落在Node3上
    在这里插入图片描述
为什么redis集群的最大槽数是16384个

CRC16算法产生的hash值有16bit,该算法可以产生2^16=65536个值。
换句话说值是分布在0~65535之间,有更大的65536不用为什么只用16384就够?
作者在做mod运算的时候,为什么不mod65536,而选择mod16384? HASH_SLOT = CRC16(key) mod 65536为什么没启用

  • 说明1

正常的心跳数据包带有节点的完整配置,可以用幂等方式用旧的节点替换旧节点,以便更新旧的配置。
这意味着它们包含原始节点的插槽配置,该节点使用2k的空间和16k的插槽,但是会使用8k的空间(使用65k的插槽)。
同时,由于其他设计折衷,Redis集群不太可能扩展到1000个以上的主节点。
因此16k处于正确的范围内,以确保每个主机具有足够的插槽,最多可容纳1000个矩阵,但数量足够少,可以轻松地将插槽配置作为原始位图传播。请注意,在小型群集中,位图将难以压缩,因为当N较小时,位图将设置的slot / N位占设置位的很大百分比。
在这里插入图片描述

  • 说明2
  1. 如果槽位为65536,发送心跳信息的消息头达8k,发送的心跳包过于庞大。
    在消息头中最占空间的是myslots[CLUSTER_SLOTS/8]。 当槽位为65536时,这块的大小是: 65536÷8÷1024=8kb
    在消息头中最占空间的是myslots[CLUSTER_SLOTS/8]。 当槽位为16384时,这块的大小是: 16384÷8÷1024=2kb
    因为每秒钟,redis节点需要发送一定数量的ping消息作为心跳包,如果槽位为65536,这个ping消息的消息头太大了,浪费带宽。
  2. redis的集群主节点数量基本不可能超过1000个。
    集群节点越多,心跳包的消息体内携带的数据越多。如果节点过1000个,也会导致网络拥堵。因此redis作者不建议redis cluster节点数量超过1000个。 那么,对于节点数在1000以内的redis cluster集群,16384个槽位够用了。没有必要拓展到65536个。
  3. 槽位越小,节点少的情况下,压缩比高,容易传输
    Redis主节点的配置信息中它所负责的哈希槽是通过一张bitmap的形式来保存的,在传输过程中会对bitmap进行压缩,但是如果bitmap的填充率slots / N很高的话(N表示节点数),bitmap的压缩率就很低。 如果节点数很少,而哈希槽数量很多的话,bitmap的压缩率就很低。
  • 计算结论
    在这里插入图片描述

docker集群环境案例实操

3主3从redis集群配置

跳转查看

主从容错切换迁移案例
  • 容错切换迁移
  • 主6382和从机切换,先停止主机6382
  • 再次查看集群信息,本次6382主6384从
    在这里插入图片描述
  • 停止主机6382,再次查看集群信息
    在这里插入图片描述
    6382宕机,6386成为新的master
  • 6386上位并且能正常使用
    在这里插入图片描述
    在这里插入图片描述
  • 6382重新启动,成为6386的从节点
    在这里插入图片描述
  • Redis集群不保证强─致性,这意味着在特定的条件下,Redis集群可能会丢掉一些被系统收到的写入请求命令
  • 手动故障转移or节点从属调整该如何处理

重新登入6382
cluster failover
在这里插入图片描述

主从扩容案例

跳转查看

主从缩容案例(6387,6388下线)

跳转查看

集群常见操作命令和CRC16算法分析

  • 不在同一个slot槽位下的多键操作支持不好,通识占位符登场

在这里插入图片描述
不在同一个slot槽位下的键值无法使用mset、mget等多键操作
可以通过{}来定义同一个组的概念,使key中{}内相同内容的键值对放到一个slot槽位去,对照下图类似k1k2k3都映射为x,自然槽位一样
在这里插入图片描述

  • crc16

Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽。集群的每个节点负责一部分hash槽

  • 常用命令

在这里插入图片描述
默认YES,现在集群架构是3主3从的redis cluster由3个master平分16384个slot,每个master的小集群负责1/3的slot,对应一部分数据。
cluster-require-full-coverage: 默认值 yes , 即需要集群完整性,方可对外提供服务 通常情况,如果这3个小集群中,任何一个(1主1从)挂了,你这个集群对外可提供的数据只有2/3了, 整个集群是不完整的, redis 默认在这种情况下,是不会对外提供服务的。
如果你的诉求是,集群不完整的话也需要对外提供服务,需要将该参数设置为no ,这样的话你挂了的那个小集群是不行了,但是其他的小集群仍然可以对外提供服务。

  • CLUSTER COUNTKEYSINSLOT槽位数字编号
    1,该槽位被占用
    2,该槽位没占用
  • CLUSTER KEYSLOT键名称
    该键应该存在哪个槽位上

Springboot集成Redis

集成Jedis

  • 是什么

Jedis Client是Redis官网推荐的一个面向java客户端,库文件实现了对各类API进行封装调用

  • 步骤
  • 修改pom文件
   <parent>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-parent</artifactId>
       <version>2.6.10</version>
       <relativePath/>
   </parent>
   <properties>
       <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
       <maven.compiler.source>1.8</maven.compiler.source>
       <maven.compiler.target>1.8</maven.compiler.target>
       <junit.version>4.12</junit.version>
       <log4j.version>1.2.17</log4j.version>
       <lombok.version>1.16.18</lombok.version>
   </properties>
    <dependencies>
       <!--SpringBoot通用依赖模块-->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
       </dependency>
       <!--jedis-->
       <dependency>
           <groupId>redis.clients</groupId>
           <artifactId>jedis</artifactId>
           <version>4.3.1</version>
       </dependency>
       <!--通用基础配置-->
       <dependency>
           <groupId>junit</groupId>
           <artifactId>junit</artifactId>
           <version>${junit.version}</version>
       </dependency>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-test</artifactId>
           <scope>test</scope>
       </dependency>
       <dependency>
           <groupId>log4j</groupId>
           <artifactId>log4j</artifactId>
           <version>${log4j.version}</version>
       </dependency>
       <dependency>
           <groupId>org.projectlombok</groupId>
           <artifactId>lombok</artifactId>
           <version>${lombok.version}</version>
           <optional>true</optional>
       </dependency>
   </dependencies>

   <build>
       <plugins>
           <plugin>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-maven-plugin</artifactId>
           </plugin>
       </plugins>
   </build>
  • 修改yml文件
server:
 port: 7777
spring:
 application:
   name: redis7_study
  • 主启动类
@SpringBootApplication
public class Redis7Study7777 {
   public static void main(String[] args) {
       SpringApplication.run(Redis7Study7777.class, args);
   }
}
  • 业务类
public class JedisDemo {
   public static void main(String[] args) {
      Jedis jedis = new Jedis("192.168.183.139", 6379);

       jedis.auth("123456");


      System.out.println("redis ping retvalue: " + jedis.ping());

       jedis.set("k1", "v1");

       System.out.println("k1, value: " + jedis.get("k1"));
   }
}

在这里插入图片描述

集成lettuce

  • lettuce vs jedis
    在这里插入图片描述

  • 案例

改pom

	   <!--lettuce-->
        <dependency>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
            <version>6.2.1.RELEASE</version>
        </dependency>

业务类

public class LettuceDemo {
   public static void main(String[] args) {
       //使用构建器 RedisURI.builder
       RedisURI uri = RedisURI.builder()
               .redis("192.168.183.139")
               .withPort(6379)
               .withAuthentication("default", "123456")
               .build();
       //创建连接客户端
       RedisClient client = RedisClient.create(uri);
       StatefulRedisConnection<String, String> connect = client.connect();
       // 操作命令api
       RedisCommands<String, String> commands = connect.sync();

       // keys
       for (String key : commands.keys("*")) {
           System.out.print(key + " ");
       }
       System.out.println();

       // string
       commands.set("k4", "lettuce");
       System.out.println("k4: " + commands.get("k4"));

       // list
//        commands.lpush("list2", "a", "b", "c", "d", "d");
       for (String s : commands.lrange("list2", 0, -1)) {
           System.out.print(s + " ");
       }

       System.out.println();
       // 关闭
       connect.close();
       client.shutdown();
   }
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

集成RedisTemplate(推荐使用)

连接单机
  • pom
<!--SpringBoot与Redis整合依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <!--swagger2-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
  • yml
server.port=7777

spring.application.name=redis7_study

# ========================logging=====================
logging.level.root=info
logging.level.com.atguigu.redis7=info
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger- %msg%n 

logging.file.name=D:/mylogs2023/redis7_study.log
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger- %msg%n

# ========================swagger=====================
spring.swagger2.enabled=true
#在springboot2.6.X结合swagger2.9.X会提示documentationPluginsBootstrapper空指针异常,
#原因是在springboot2.6.X中将SpringMVC默认路径匹配策略从AntPathMatcher更改为PathPatternParser,
# 导致出错,解决办法是matching-strategy切换回之前ant_path_matcher
spring.mvc.pathmatch.matching-strategy=ant_path_matcher

# ========================redis单机=====================
spring.redis.database=0
# 修改为自己真实IP
spring.redis.host=192.168.111.185
spring.redis.port=6379
spring.redis.password=111111
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
  • 主启动
@SpringBootApplication
public class Redis7Study7777
{
    public static void main(String[] args)
    {
        SpringApplication.run(Redis7Study7777.class,args);
    }
}

  • 业务类
    配置类
@Configuration
public class RedisConfig
{
    /**
     * redis序列化的工具配置类,下面这个请一定开启配置
     * 127.0.0.1:6379> keys *
     * 1) "ord:102"  序列化过
     * 2) "\xac\xed\x00\x05t\x00\aord:102"   野生,没有序列化过
     * this.redisTemplate.opsForValue(); //提供了操作string类型的所有方法
     * this.redisTemplate.opsForList(); // 提供了操作list类型的所有方法
     * this.redisTemplate.opsForSet(); //提供了操作set的所有方法
     * this.redisTemplate.opsForHash(); //提供了操作hash表的所有方法
     * this.redisTemplate.opsForZSet(); //提供了操作zset的所有方法
     * @param lettuceConnectionFactory
     * @return
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory)
    {
        RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();

        redisTemplate.setConnectionFactory(lettuceConnectionFactory);
        //设置key序列化方式string
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //设置value的序列化方式json,使用GenericJackson2JsonRedisSerializer替换默认序列化
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());

        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());

        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }
}
@Configuration
@EnableSwagger2
public class SwaggerConfig
{
    @Value("${spring.swagger2.enabled}")
    private Boolean enabled;

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .enable(enabled)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.atguigu.redis7")) //你自己的package
                .paths(PathSelectors.any())
                .build();
    }
    public ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("springboot利用swagger2构建api接口文档 "+"\t"+ DateTimeFormatter.ofPattern("yyyy-MM-dd").format(LocalDateTime.now()))
                .description("springboot+redis整合,有问题给管理员阳哥邮件:zzyybs@126.com")
                .version("1.0")
                .termsOfServiceUrl("https://www.atguigu.com/")
                .build();
    }
}

service

@Service
@Slf4j
public class OrderService
{
    public static final String ORDER_KEY = "order:";

    @Resource
    private RedisTemplate redisTemplate;

    public void addOrder()
    {
        int keyId = ThreadLocalRandom.current().nextInt(1000)+1;
        String orderNo = UUID.randomUUID().toString();
        redisTemplate.opsForValue().set(ORDER_KEY+keyId,"京东订单"+ orderNo);
        log.info("=====>编号"+keyId+"的订单流水生成:{}",orderNo);
    }

    public String getOrderById(Integer id)
    {
        return (String)redisTemplate.opsForValue().get(ORDER_KEY + id);
    }
}

controller

@Api(tags = "订单接口")
@RestController
@Slf4j
public class OrderController
{
    @Resource
    private OrderService orderService;

    @ApiOperation("新增订单")
    @RequestMapping(value = "/order/add",method = RequestMethod.POST)
    public void addOrder()
    {
        orderService.addOrder();
    }


    @ApiOperation("按orderId查订单信息")
    @RequestMapping(value = "/order/{id}", method = RequestMethod.GET)
    public String findUserById(@PathVariable Integer id)
    {
        return orderService.getOrderById(id);
    }
}
  • 测试

序列化问题
在这里插入图片描述
默认情况下,RedisTemplate使用JDK序列化方式,会出现问题
可以使用StringRedisTemplate或者写Redis配置文件

连接集群
  • 本地添加路由,对容器的访问通过虚拟机来路由
route -p add 172.17.0.0 mask 255.255.255.0 192.168.183.139
  • 启动6台redis集群
  • 修改yml
server.port=7777

spring.application.name=redis7_study

# ========================logging=====================
logging.level.root=info
logging.level.com.atguigu.redis7=info
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger- %msg%n 

logging.file.name=D:/mylogs2023/redis7_study.log
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger- %msg%n

# ========================swagger=====================
spring.swagger2.enabled=true
#在springboot2.6.X结合swagger2.9.X会提示documentationPluginsBootstrapper空指针异常,
#原因是在springboot2.6.X中将SpringMVC默认路径匹配策略从AntPathMatcher更改为PathPatternParser,
# 导致出错,解决办法是matching-strategy切换回之前ant_path_matcher
spring.mvc.pathmatch.matching-strategy=ant_path_matcher


# ========================redis集群=====================
spring.redis.password=111111
# 获取失败 最大重定向次数
spring.redis.cluster.max-redirects=3
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
spring.redis.cluster.nodes=192.168.111.175:6381,192.168.111.175:6382,192.168.111.172:6383,192.168.111.172:6384,192.168.111.174:6385,192.168.111.174:6386
  • 访问集群

http://localhost:7777/swagger-ui.html#/

问题
  • 人为模拟,master-6381机器意外宕机,手动shutdown
  • 先对redis集群命令方式,手动验证各种读写命令,看看6384是否上位
  • Redis Ciuster集群能自动感知并自动完成主备切换,对应的slave6384会被选举为新的master节点
  • 微服务客户端再次访问读写测试
  • 故障现象
    SpringBoot客户端没有动态感知到RedisCluster的最新集群信息
    Redis Cluster集群部署采用了3主3从拓扑结构,数据读写访问master节点, slave节点负责备份。当master宕机主从切换成功,redis手动OK,but 2个经典故障
    在这里插入图片描述
  • 导致原因
    SpringBoot 2.X版本,Redis默认的连接池采用Lettuce
    当Redis集群节点发生变化后,Letture默认是不会刷新节点拓扑
  • 解决方案
    刷新节点集群拓扑动态感应
  • 修改yml
# ========================redis集群=====================
spring.redis.password=111111
# 获取失败 最大重定向次数
spring.redis.cluster.max-redirects=3
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
#支持集群拓扑动态感应刷新,自适应拓扑刷新是否使用所有可用的更新,默认false关闭
spring.redis.lettuce.cluster.refresh.adaptive=true
#定时刷新
spring.redis.lettuce.cluster.refresh.period=2000
spring.redis.cluster.nodes=192.168.111.175:6381,192.168.111.175:6382,192.168.111.172:6383,192.168.111.172:6384,192.168.111.174:6385,192.168.111.174:6386

高阶篇

Redis单线程 vs 多线程(入门篇)

Redis为何选择单线程

这种问法是不严谨的,为什么这么说
Redis的版本很多3.x、4.x、6.x,版本不同架构也是不同的,不限定版本问是否单线程也不太严谨。
1 版本3.x ,最早版本,也就是大家口口相传的redis是单线程
2 版本4.x,严格意义来说也不是单线程,而是负责处理客户端请求的线程是单线程,但是开始加了点多线程的东西(异步删除)。—貌似
3 2020年5月版本的6.0.x后及2022年出的7.0版本后,告别了大家印象中的单线程,用一种全新的多线程来解决问题。—实锤

有几个里程碑式的重要版本
在这里插入图片描述

Redis是单线程
主要是指Redis的网络IO和键值对读写是由一个线程来完成的,Redis在处理客户端的请求时包括获取 (socket 读)、解析、执行、内容返回 (socket 写) 等都由一个顺序串行的主线程处理,这就是所谓的“单线程”。这也是Redis对外提供键值存储服务的主要流程。
在这里插入图片描述
但Redis的其他功能,比如持久化RDB、AOF、异步删除、集群数据同步等等,其实是由额外的线程执行的。
Redis命令工作线程是单线程的,但是,整个Redis来说,是多线程的;

Redis3.x单线程时代但性能依旧很快的主要原因

  • 基于内存操作:Redis的所有数据都存在内存中,因此所有的运算都是内存级别的,所以他的性能比较高;
  • 数据结构简单: Redis的数据结构是专门设计的,而这些简单的数据结构的查找和操作的时间大部分复杂度都是O(1),因此性能比较高;
  • 多路复用和非阻塞I/O: Redis使用I/O多路复用功能来监听多个socket连接客户端,这样就可以使用一个线程连接来处理多个请求,减少线程切换带来的开销,同时也避免了V/O阻塞操作
  • 避免上下文切换:因为是单线程模型,因此就避免了不必要的上下文切换和多线程竞争,这就省去了多线程切换带来的时间和性能上的消耗,而且单线程不会导致死锁问题的发生

简单来说,Redis4.0之前一直采用单线程的主要原因有以下三个

  1. 使用单线程模型是 Redis 的开发和维护更简单,因为单线程模型方便开发和调试;
  2. 即使使用单线程模型也并发的处理多客户端的请求,主要使用的是IO多路复用和非阻塞IO;
  3. 对于Redis系统来说,主要的性能瓶颈是内存或者网络带宽而并非 CPU。

为何逐渐加入了多线程特性

单线程的问题

正常情况下使用 del 指令可以很快的删除数据,而当被删除的 key 是一个非常大的对象时,例如时包含了成千上万个元素的 hash 集合时,那么 del 指令就会造成 Redis 主线程卡顿。
这就是redis3.x单线程时代最经典的故障,大key删除的头疼问题
由于redis是单线程的,del bigKey …
等待很久这个线程才会释放,类似加了一个synchronized锁,你可以想象高并发下,程序堵成什么样子?

如何解决

使用惰性删除可以有效的避免 Redis卡顿的问题
比如当我(Redis)需要删除一个很大的数据时,因为是单线程原子命令操作,这就会导致 Redis 服务卡顿,
于是在 Redis 4.0 中就新增了多线程的模块,当然此版本中的多线程主要是为了解决删除数据效率比较低的问题的。

  • unlink key
  • flushdb async
  • flushall async
  • 把删除工作交给了后台的小弟(子线程)异步来删除数据了。

因为Redis是单个主线程处理,redis之父antirez一直强调"Lazy Redis is better Redis".
而lazy free的本质就是把某些cost(主要时间复制度,占用主线程cpu时间片)较高删除操作,
从redis主线程剥离让bio子线程来处理,极大地减少主线阻塞时间。从而减少删除导致性能和稳定性问题。
在Redis 4.0就引入了多个线程来实现数据的异步惰性删除等功能,
但是其处理读写请求的仍然只有一个线程,所以仍然算是狭义上的单线程。

redis6/7的多线程特性和IO多路复用入门篇

redis的瓶颈:网络IO

在Redis6/7中,非常受关注的第一个新特性就是多线程。
这是因为,Redis一直被大家熟知的就是它的单线程架构,虽然有些命令操作可以用后台线程或子进程执行(比如数据删除、快照生成、AOF重写)。但是,从网络IO处理到实际的读写命令处理,都是由单个线程完成的。
随着网络硬件的性能提升,Redis的性能瓶颈有时会出现在网络IO的处理上,也就是说,单个主线程处理网络请求的速度跟不上底层网络硬件的速度,
为了应对这个问题:
采用多个IO线程来处理网络请求,提高网络请求处理的并行度,Redis6/7就是采用的这种方法
但是,Redis的多IO线程只是用来处理网络请求的,对于读写操作命令Redis仍然使用单线程来处理。这是因为,Redis处理请求时,网络处理经常是瓶颈,通过多个IO线程并行处理网络操作,可以提升实例的整体处理性能。而继续使用单线程执行命令操作,就不用为了保证Lua脚本、事务的原子性,额外开发多线程互斥加锁机制了(不管加锁操作处理),这样一来,Redis线程模型实现就简单了

Unix网络编程五种IO模型
  • Blocking IO - 阻塞IO
  • NoneBlocking IO - 非阻塞IO
  • signal driven IO - 信号驱动IO
  • asynchronous IO - 异步IO
  • IO multiplexing - IO多路复用

Linux世界一切皆文件
文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
在这里插入图片描述

IO多路复用是什么
—种同步的Io模型,实现一个线程监视多个文件句柄,—旦某个文件句柄就绪就能够通知到对应应用程序进行相应的读写操作,没有文件句柄就绪时就会阻塞应用程序,从而释放CPU资源
I/O∶网络I/O,尤其在操作系统层面指数据在内核态和用户态之间的读写操作
多路:多个客户端连接(连接就是套接字描述符,即socket或者channel)
复用:复用一个或几个线程。
IO多路复用: 也就是说一个或一组线程处理多个TCP连接,使用单进程就能够实现同时处理多个客户端的连接,无需创建或者维护过多的进程/线程
—个服务端进程可以同时处理多个套接字描述符。
实现IO多路复用的模型有3种:可以分select->poll->epoll三个阶段来描述。

场景体验,引出epoll
模拟一个tcp服务器处理30个客户socket。
假设你是一个监考老师,让30个学生解答一道竞赛考题,然后负责验收学生答卷,你有下面几个选择:
第一种选择(轮询):按顺序逐个验收,先验收A,然后是B,之后是C、D。。。这中间如果有一个学生卡住,全班都会被耽误,你用循环挨个处理socket,根本不具有并发能力。
第二种选择(来一个new一个,1对1服务):你创建30个分身线程,每个分身线程检查一个学生的答案是否正确。 这种类似于为每一个用户创建一个进程或者线程处理连接。
第三种选择(响应式处理,1对多服务),你站在讲台上等,谁解答完谁举手。这时C、D举手,表示他们解答问题完毕,你下去依次检查C、D的答案,然后继续回到讲台上等。此时E、A又举手,然后去处理E和A。。。这种就是IO复用模型。Linux下的select、poll和epoll就是干这个的。
将用户socket对应的文件描述符(FileDescriptor)注册进epoll,然后epoll帮你监听哪些socket上有消息到达,这样就避免了大量的无用操作。此时的socket应该采用非阻塞模式。这样,整个过程只在调用select、poll、epoll这些调用的时候才会阻塞,收发客户消息是不会阻塞的,整个进程或者线程就被充分利用起来,这就是事件驱动,所谓的reactor反应模式。
在这里插入图片描述
在单个线程通过记录跟踪每一个Sockek(I/O流)的状态来同时管理多个I/O流. 一个服务端进程可以同时处理多个套接字描述符。
目的是尽量多的提高服务器的吞吐能力。
大家都用过nginx,nginx使用epoll接收请求,ngnix会有很多链接进来, epoll会把他们都监视起来,然后像拨开关一样,谁有数据就拨向谁,然后调用相应的代码处理。redis类似同理,这就是IO多路复用原理,有请求就响应,没请求不打扰。

总结
服务端进程可以同时处理多个套接字描述符连接
在这里插入图片描述

总结

redis工作线程是单线程的,但是整个redis来说,是多线程的

主线程和IO线程是怎么协作完成请求处理的-精简版

I/O 的读和写本身是堵塞的,比如当 socket 中有数据时,Redis 会通过调用先将数据从内核态空间拷贝到用户态空间,再交给 Redis 调用,而这个拷贝的过程就是阻塞的,当数据量越大时拷贝所需要的时间就越多,而这些操作都是基于单线程完成的。
在这里插入图片描述
从Redis6开始,就新增了多线程的功能来提高 I/O 的读写性能,他的主要实现思路是将主线程的 IO 读写任务拆分给一组独立的线程去执行,这样就可以使多个 socket 的读写可以并行化了,采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),将最耗时的Socket的读取、请求解析、写入单独外包出去,剩下的命令执行仍然由主线程串行执行并和内存的数据交互。
在这里插入图片描述
结合上图可知,网络IO操作就变成多线程化了,其他核心部分仍然是线程安全的,是个不错的折中办法。

结论

Redis6→7将网络数据读写、请求协议解析通过多个IO线程的来处理
对于真正的命令执行来说,仍然使用主线程操作,一举两得
在这里插入图片描述

Redis7多线程

如果你在实际应用中,发现Redis实例的CPU开销不大但吞吐量却没有提升,可以考虑使用Redis7的多线程机制,加速网络处理,进而提升实例的吞吐量
在Redis6.0及7后,多线程机制默认是关闭的,如果需要使用多线程功能,需要在redis.conf中完成两个设置
在这里插入图片描述
在这里插入图片描述
1.设置io-thread-do-reads配置项为yes,表示启动多线程。
。设置线程个数。关于线程数的设置,官方的建议是如果为 4 核的 CPU,建议线程数设置为 2 或 3,如果为 8 核 CPU 建议线程数设置为 6,线程数一定要小于机器核数,线程数并不是越大越好。

BigKey

MoreKey案例

大批量往redis里面插入2000W测试数据key
  • Linux Bash下面执行,插入100w

在这里插入图片描述
#生成100W条redis批量设置kv的语句(key=kn,value=vn)写入到/tmp目录下的redisTest.txt文件中

for((i=1;i<=100*10000;i++)); do echo "set k$i v$i" >> /tmp/redisTest.txt ;done;
  • 通过redis提供的管道–pipe命令插入100W大批量数据

结合自己机器的地址:
cat /tmp/redisTest.txt | /opt/redis-7.0.0/src/redis-cli -h 127.0.0.1 -p 6379 -a 111111 --pipe
在这里插入图片描述

某快递巨头真实生产案例新闻

在这里插入图片描述

  • 测试keys * 100w花费多少秒遍历查询

在这里插入图片描述
key * 这个指令有致命的弊端,在实际环境中最好不要使用
在这里插入图片描述

  • 生产上限制keys */flushdb/flushall等危险命令以防止误删误用?

通过配置设置禁用这些命令,redis.conf在SECURITY这一项中
在这里插入图片描述

不用keys *避免卡顿,那该用什么
  • scan命令

类似Mysql limit但不完全相同

  • scan命令用于迭代数据库中的数据库键

在这里插入图片描述
在这里插入图片描述
SCAN 命令是一个基于游标的迭代器,每次被调用之后, 都会向用户返回一个新的游标, 用户在下次迭代时需要使用这个新游标作为 SCAN 命令的游标参数, 以此来延续之前的迭代过程。
SCAN 返回一个包含两个元素的数组
第一个元素是用于进行下一次迭代的新游标,
第二个元素则是一个数组, 这个数组中包含了所有被迭代的元素。如果新游标返回零表示迭代已结束
SCAN的遍历顺序
非常特别,它不是从第一维数组的第零位一直遍历到末尾,而是采用了高位进位加法来遍历。之所以使用这样特殊的方式进行遍历,是考虑到字典的扩容和缩容时避免槽位的遍历重复和遗漏。
在这里插入图片描述

BigKey案例

多大算big
  • 参考《阿里云Redis开发规范》

在这里插入图片描述

  • string和二级结构

string是value,最大512MB但是≥10KB就是bigkey
list、hash、set和zset,个数超过5000就是bigkey

哪些危害
  • 内存不均,集群迁移困难
  • 超时删除,大key删除作梗
  • 网络流量阻塞
如何产生
  • 社交类: 王心凌粉丝列表,典型案例粉丝逐步递增
  • 汇总统计: 某个报表,月日年经年累月的积累
如何发现
  • redis-cli --bigkeys

给出每种数据结构Top 1 bigkey,同时给出每种数据类型的键值个数+平均大小
想查询大于10kb的所有key,–bigkeys参数就无能为力了,需要用到memory usage来计算每个键值的字节数
redis-cli -h 127.0.0.1 -p 6379 -a 123456 --bigkeys
每隔 100 条 scan 指令就会休眠 0.1s,ops 就不会剧烈抬升,但是扫描的时间会变长
redis-cli -h 127.0.0.1 -p 7001 –-bigkeys -i 0.1
在这里插入图片描述

  • memory usage 键

在这里插入图片描述
计算每个键值的字节数

如何删除
  • 参考《阿里云Redis开发规范》

在这里插入图片描述

  • 普通命令

String
—般用del,如果过于庞大unlink

hash
使用hscan每次获取少量field-value,再使用hdel删除每个field
在这里插入图片描述
在这里插入图片描述

list
使用ltrim渐进式逐步删除,直到全部删除完成
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

set
使用sscan每次获取部分元素,再使用srem命令删除每个元素
在这里插入图片描述
在这里插入图片描述

zset
使用zscan每次获取部分元素,再使用ZREMRANGEBYRANK命令删除每个元素
在这里插入图片描述
在这里插入图片描述

BigKey生产调优

redis.conf配置文件LAZY FREEING相关说明

  • 非阻塞和阻塞删除命令
    在这里插入图片描述
  • 优化配置
    在这里插入图片描述

缓存双写一致性之更新策略探讨

缓存双写一致性

  • 如果redis有数据

需要和数据库种的值相同

  • 如果redis无数据

数据库种的值要是最新值,且准备回写redis

  • 缓存按照操作来分,细分2种

只读缓存
读写缓存

  • 同步直写策略
    写数据库后也同步写redis缓存,缓存和数据库中的数据一致;
    对于读写缓存来说,要想保证缓存和数据库中的数据一致,就要采用同步直写策略
  • 异步缓写策略
    正常业务运行中,mysq|数据变动了,但是可以在业务上容许出现一定时间后才作用于redis,比如仓库、物流系统
    异常情况出现了,不得不将失败的动作重新修补,有可能需要借助kafka或者RabbitMQ等消息中间件,实现重试重写
  • 代码演示
    在这里插入图片描述
  • 采用双检加锁策略

多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个 互斥锁来锁住它。
其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。
后面的线程进来发现已经有缓存了,就直接走缓存。
在这里插入图片描述

@Service
@Slf4j
public class UserService {
    public static final String CACHE_KEY_USER = "user:";
    @Resource
    private UserMapper userMapper;
    @Resource
    private RedisTemplate redisTemplate;

    /**
     * 业务逻辑没有写错,对于小厂中厂(QPS《=1000)可以使用,但是大厂不行
     * @param id
     * @return
     */
    public User findUserById(Integer id)
    {
        User user = null;
        String key = CACHE_KEY_USER+id;

        //1 先从redis里面查询,如果有直接返回结果,如果没有再去查询mysql
        user = (User) redisTemplate.opsForValue().get(key);

        if(user == null)
        {
            //2 redis里面无,继续查询mysql
            user = userMapper.selectByPrimaryKey(id);
            if(user == null)
            {
                //3.1 redis+mysql 都无数据
                //你具体细化,防止多次穿透,我们业务规定,记录下导致穿透的这个key回写redis
                return user;
            }else{
                //3.2 mysql有,需要将数据写回redis,保证下一次的缓存命中率
                redisTemplate.opsForValue().set(key,user);
            }
        }
        return user;
    }


    /**
     * 加强补充,避免突然key失效了,打爆mysql,做一下预防,尽量不出现击穿的情况。
     * @param id
     * @return
     */
    public User findUserById2(Integer id)
    {
        User user = null;
        String key = CACHE_KEY_USER+id;

        //1 先从redis里面查询,如果有直接返回结果,如果没有再去查询mysql,
        // 第1次查询redis,加锁前
        user = (User) redisTemplate.opsForValue().get(key);
        if(user == null) {
            //2 大厂用,对于高QPS的优化,进来就先加锁,保证一个请求操作,让外面的redis等待一下,避免击穿mysql
            synchronized (UserService.class){
                //第2次查询redis,加锁后
                user = (User) redisTemplate.opsForValue().get(key);
                //3 二次查redis还是null,可以去查mysql了(mysql默认有数据)
                if (user == null) {
                    //4 查询mysql拿数据(mysql默认有数据)
                    user = userMapper.selectByPrimaryKey(id);
                    if (user == null) {
                        return null;
                    }else{
                        //5 mysql里面有数据的,需要回写redis,完成数据一致性的同步工作
                        redisTemplate.opsForValue().setIfAbsent(key,user,7L,TimeUnit.DAYS);
                    }
                }
            }
        }
        return user;
    }

}

数据库和缓存一致性的几种更新策略

目的

总之,要达成最终一致性
给缓存设置过期时间,定期清理缓存并回写,是保证最终一致性的解决方案
我们可以对存入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努力即可。也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存,达到一致性,切记,要以mysql的数据库写入库为准。

四种更新策略
  • 先更新数据库,再更新缓存
  • 问题1
    1 先更新mysql的某商品的库存,当前商品的库存是100,更新为99个
    2 先更新mysql修改为99成功,然后更新redis
    3 此时假设异常出现,更新redis失败了,这导致mysql里面的库存是99而redis里面的还是100
    4 上述发生,会让数据库里面和缓存redis里面数据不一致,读到redis脏数据
  • 问题2
    【先更新数据库,再更新缓存】,A、B两个线程发起调用
    【正常逻辑】
    1 A update mysql 100
    2 A update redis 100
    3 B update mysql 80
    4 B update redis 80
    =============================
    【异常逻辑】多线程环境下,A、B两个线程有快有慢,有前有后有并行
    1 A update mysql 100
    3 B update mysql 80
    4 B update redis 80
    2 A update redis 100
    =============================
    最终结果,mysql和redis数据不一致
    mysql80,redis100
  • 先更新缓存,再更新数据库

不推荐: 业务上一般把mysql作为底单数据库,保证最后解释
先更新缓存,再更新数据库】,A、B两个线程发起调用
【正常逻辑】
1 A update redis 100
2 A update mysql 100
3 B update redis 80
4 B update mysql 80
====================================
【异常逻辑】多线程环境下,A、B两个线程有快有慢有并行
A update redis 100
B update redis 80
B update mysql 80
A update mysql 100
----mysql100,redis80

  • 先删除缓存,再更新数据库
  • 问题
    (1)请求A进行写操作,删除redis缓存后,工作正在进行中,更新mysql…A还么有彻底更新完mysql,还没commit
    (2)请求B开工查询,查询redis发现缓存不存在(被A从redis中删除了)
    (3)请求B继续,去数据库查询得到了mysql中的旧值(A还没有更新完)
    (4)请求B将旧值写回redis缓存
    (5)请求A将新值写入mysql数据库
    上述情况就会导致不一致的情形出现。
时间线程A线程B出现的问题
t1请求A进行写操作,删除缓存成功后,工作正在mysql进行中…
t21 缓存中读取不到,立刻读mysql,由于A还没有对mysql更新完,读到的是旧值2 还把从mysql读取的旧值,写回了redis1. A还没有更新完mysql,导致B读到了旧值 2. 线程B遵守回写机制,把旧值写回redis,导致其它请求读取的还是旧值,A白干了。
t3A更新完mysql数据库的值,overredis是被B写回的旧值,mysql是被A更新的新值。出现了,数据不一致问题。
  • 解决方案
    采用延迟双删
    在这里插入图片描述
    在这里插入图片描述
  • 先更新数据库,再删除缓存
  • 问题
    先更新数据库,再删除缓存
时间线程A线程B出现的问题
t1更新数据库中的值…
t2缓存中立刻命中,此时B读取的是缓存旧值A还没有来得及删除缓存的值,导致B缓存命中读到旧值
t3更新缓存的数据,over
假如缓存删除失败或者来不及,导致请求再次访问redis时缓存命中,读取到的是缓存旧值。
  • 解决方案
    在这里插入图片描述
    1 可以把要删除的缓存值或者是要更新的数据库值暂存到消息队列中(例如使用Kafka/RabbitMQ等)。
    2 当程序没有能够成功地删除缓存值或者是更新数据库值时,可以从消息队列中重新读取这些值,然后再次进行删除或更新。
    3 如果能够成功地删除或更新,我们就要把这些值从消息队列中去除,以免重复操作,此时,我们也可以保证数据库和缓存的数据一致了,否则还需要再次进行重试
    4 如果重试超过的一定次数后还是没有成功,我们就需要向业务层发送报错信息了,通知运维人员。
  • 类似经典的分布式事务问题,只有一个权威答案
    最终一致性
    流量充值,先下发短信实际充值可能滞后5分钟,可以接受
    电商发货,短信下发但是物流明天见
总结
  • 如何选择方案?利弊如何

在大多数业务场景下,
优先使用先更新数据库,再删除缓存的方案(先更库→后删存)。理由如下:
1 先删除缓存值再更新数据库,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力导致打满mysql。
2 如果业务应用中读取数据库和写缓存的时间不好估算,那么,延迟双删中的等待时间就不好设置。

  • 总结

在这里插入图片描述

Redis与MySQL数据双写一致性工程落地案例

canal

  • 是什么

canal [ka’nael],译意为水道/管道/沟渠,主要用途是基于MySQL数据库增量日志解析,提供增量数据订阅和消费

  • 能干嘛
  • 数据库镜像
  • 数据库实时备份
  • 索引构建和实时维护(拆分异构索引、倒排索引等)
  • 业务cache刷新
  • 带业务逻辑的增量数据处理
  • 下载

https://github.com/alibaba/canal/releases/tag/canal-1.1.6
docker下载

  1. 先拉取canal镜像,默认版本latest
    docker pull canal/canal-server
  2. 先启动canal,主要是为了copy出配置文件
    docker cp canal:/home/admin/canal-server/conf/canal.properties /docker-v/canal/conf
    docker cp canal:/home/admin/canal-server/conf/example/instance.properties /docker-v/canal/conf
  3. 文件copy完成后主要是修改instance这个文件。第一个红框是你需要监听数据库的地址和端口;第二个红框是你数据库的用户和密码,这个用户信息一定是要有全部权限的用户,非root用户;第三个是匹配数据表的规则
    在这里插入图片描述
  4. 修改完成后 ,将之前的canal容器关闭,重新起一个新的容器
    docker stop canal
    docker rm canal
docker run --name canal -p 11111:11111 -d \
-v /docker-v/canal/conf/instance.properties:/home/admin/canal-server/conf/example/instance.properties \
-v /docker-v/canal/conf/canal.properties:/home/admin/canal-server/conf/canal.properties \
-v /docker-v/canal/logs/:/home/admin/canal-server/logs/ \
canal/canal-server

工作原理

  • 传统MySQL主从复制工作原理

在这里插入图片描述
MySQL的主从复制将经过如下步骤:
1、当 master 主服务器上的数据发生改变时,则将其改变写入二进制事件日志文件中;
2、salve 从服务器会在一定时间间隔内对 master 主服务器上的二进制日志进行探测,探测其是否发生过改变,如果探测到 master 主服务器的二进制事件日志发生了改变,则开始一个 I/O Thread 请求 master 二进制事件日志;
3、同时 master 主服务器为每个 I/O Thread 启动一个dump Thread,用于向其发送二进制事件日志;
4、slave 从服务器将接收到的二进制事件日志保存至自己本地的中继日志文件中;
5、salve 从服务器将启动 SQL Thread 从中继日志中读取二进制日志,在本地重放,使得其数据和主服务器保持一致;
6、最后 I/O Thread 和 SQL Thread 将进入睡眠状态,等待下一次被唤醒;

  • canal工作原理

在这里插入图片描述

mysql-canal-redis双写一致性Coding

mysql
  • 查看mysql版本

select version();
5.7.24-log

  • 当前主机的二进制日志

show master status;

  • 查看SHOW VARIABLES LIKE 'log_bin';

在这里插入图片描述

  • 开启mysql的binlog写入功能

D:\mysql-5.7.24-winx64
备份my.ini

log-bin=mysql-bin #开启 binlog
binlog-format=ROW #选择 ROW 模式
server_id=1    #配置MySQL replaction需要定义,不要和canal的 slaveId重复

ROW模式 除了记录sql语句之外,还会记录每个字段的变化情况,能够清楚的记录每行数据的变化历史,但会占用较多的空间。
STATEMENT模式只记录了sql语句,但是没有记录上下文信息,在进行数据恢复的时候可能会导致数据的丢失情况;
MIX模式比较灵活的记录,理论上说当遇到了表结构变更的时候,就会记录为statement模式。当遇到了数据更新或者删除情况下就会变为row模式;
在这里插入图片描述

  • 重启mysql

SHOW VARIABLES LIKE 'log_bin'
在这里插入图片描述

  • 授权canal连接mysql账号
  • mysql默认的用户在mysql库的user表里
    ``SELECT * FROM mysql.`user```
    在这里插入图片描述
  • 默认没有canal账户,此处新建+授权
DROP USER IF EXISTS 'canal'@'%';
CREATE USER 'canal'@'%' IDENTIFIED BY 'canal';  
GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' IDENTIFIED BY 'canal';  
FLUSH PRIVILEGES; 
SELECT * FROM mysql.user;

在这里插入图片描述

canal服务端
  • 配置

修改instance.properties文件
换成自己的mysql主机master的ip地址
在这里插入图片描述
换成自己的在mysql新建的canal账号
在这里插入图片描述

  • docker运行canal
docker run --name canal -p 11111:11111 -d \
-v /docker-v/canal/conf/instance.properties:/home/admin/canal-server/conf/example/instance.properties \
-v /docker-v/canal/conf/canal.properties:/home/admin/canal-server/conf/canal.properties \
-v /docker-v/canal/logs/:/home/admin/canal-server/logs/ \
canal/canal-server
  • 查看

在这里插入图片描述
在这里插入图片描述

canal客户端(Java编写业务程序)
  • sql脚本
CREATE TABLE `t_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `userName` varchar(100) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4
  • 改pom
	<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.14</version>
        <relativePath/>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <junit.version>4.12</junit.version>
        <log4j.version>1.2.17</log4j.version>
        <lombok.version>1.16.18</lombok.version>
        <mysql.version>5.1.47</mysql.version>
        <druid.version>1.1.16</druid.version>
        <mapper.version>4.1.5</mapper.version>
        <mybatis.spring.boot.version>1.3.0</mybatis.spring.boot.version>
    </properties>

    <dependencies>
        <!--canal-->
        <dependency>
            <groupId>com.alibaba.otter</groupId>
            <artifactId>canal.client</artifactId>
            <version>1.1.0</version>
        </dependency>
        <!--SpringBoot通用依赖模块-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--swagger2-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
        <!--SpringBoot与Redis整合依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <!--SpringBoot与AOP-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
        </dependency>
        <!--Mysql数据库驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
        <!--SpringBoot集成druid连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>${druid.version}</version>
        </dependency>
        <!--mybatis和springboot整合-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>${mybatis.spring.boot.version}</version>
        </dependency>
        <!--通用基础配置junit/devtools/test/log4j/lombok/hutool-->
        <!--hutool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.2.3</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${junit.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>${log4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <optional>true</optional>
        </dependency>
        <!--persistence-->
        <dependency>
            <groupId>javax.persistence</groupId>
            <artifactId>persistence-api</artifactId>
            <version>1.0.2</version>
        </dependency>
        <!--通用Mapper-->
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper</artifactId>
            <version>${mapper.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.8.0</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
  • 写yml
server:
  port: 5555

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: 123456
    druid:
      test-while-idle: false
  • 业务类
    RedisUtils
public class RedisUtils
{
    public static final String  REDIS_IP_ADDR = "192.168.183.139";
    public static final String  REDIS_pwd = "123456";
    public static JedisPool jedisPool;

    static {
        JedisPoolConfig jedisPoolConfig=new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(20);
        jedisPoolConfig.setMaxIdle(10);
        jedisPool=new JedisPool(jedisPoolConfig,REDIS_IP_ADDR,6379,10000,REDIS_pwd);
    }

    public static Jedis getJedis() throws Exception {
        if(null!=jedisPool){
            return jedisPool.getResource();
        }
        throw new Exception("Jedispool is not ok");
    }

}

RedisCanalClientExample

import com.alibaba.fastjson.JSONObject;
import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.client.CanalConnectors;
import com.alibaba.otter.canal.protocol.CanalEntry.*;
import com.alibaba.otter.canal.protocol.Message;
import com.zdz.canal.util.RedisUtils;
import redis.clients.jedis.Jedis;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;


public class RedisCanalClientExample
{
    public static final Integer _60SECONDS = 60;
    public static final String  REDIS_IP_ADDR = "192.168.183.139";

    private static void redisInsert(List<Column> columns)
    {
        JSONObject jsonObject = new JSONObject();
        for (Column column : columns)
        {
            System.out.println(column.getName() + " : " + column.getValue() + "    update=" + column.getUpdated());
            jsonObject.put(column.getName(),column.getValue());
        }
        if(columns.size() > 0)
        {
            try(Jedis jedis = RedisUtils.getJedis())
            {
                jedis.set(columns.get(0).getValue(),jsonObject.toJSONString());
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }


    private static void redisDelete(List<Column> columns)
    {
        JSONObject jsonObject = new JSONObject();
        for (Column column : columns)
        {
            jsonObject.put(column.getName(),column.getValue());
        }
        if(columns.size() > 0)
        {
            try(Jedis jedis = RedisUtils.getJedis())
            {
                jedis.del(columns.get(0).getValue());
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }

    private static void redisUpdate(List<Column> columns)
    {
        JSONObject jsonObject = new JSONObject();
        for (Column column : columns)
        {
            System.out.println(column.getName() + " : " + column.getValue() + "    update=" + column.getUpdated());
            jsonObject.put(column.getName(),column.getValue());
        }
        if(columns.size() > 0)
        {
            try(Jedis jedis = RedisUtils.getJedis())
            {
                jedis.set(columns.get(0).getValue(),jsonObject.toJSONString());
                System.out.println("---------update after: "+jedis.get(columns.get(0).getValue()));
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }

    public static void printEntry(List<Entry> entrys) {
        for (Entry entry : entrys) {
            if (entry.getEntryType() == EntryType.TRANSACTIONBEGIN || entry.getEntryType() == EntryType.TRANSACTIONEND) {
                continue;
            }

            RowChange rowChage = null;
            try {
                //获取变更的row数据
                rowChage = RowChange.parseFrom(entry.getStoreValue());
            } catch (Exception e) {
                throw new RuntimeException("ERROR ## parser of eromanga-event has an error,data:" + entry.toString(),e);
            }
            //获取变动类型
            EventType eventType = rowChage.getEventType();
            System.out.println(String.format("================&gt; binlog[%s:%s] , name[%s,%s] , eventType : %s",
                    entry.getHeader().getLogfileName(), entry.getHeader().getLogfileOffset(),
                    entry.getHeader().getSchemaName(), entry.getHeader().getTableName(), eventType));

            for (RowData rowData : rowChage.getRowDatasList()) {
                if (eventType == EventType.INSERT) {
                    redisInsert(rowData.getAfterColumnsList());
                } else if (eventType == EventType.DELETE) {
                    redisDelete(rowData.getBeforeColumnsList());
                } else {//EventType.UPDATE
                    redisUpdate(rowData.getAfterColumnsList());
                }
            }
        }
    }


    public static void main(String[] args)
    {
        System.out.println("---------O(∩_∩)O哈哈~ initCanal() main方法-----------");

        //=================================
        // 创建链接canal服务端
        CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress(REDIS_IP_ADDR,
                11111), "example", "", "");
        int batchSize = 1000;
        //空闲空转计数器
        int emptyCount = 0;
        System.out.println("---------------------canal init OK,开始监听mysql变化------");
        try {
            connector.connect();
            //connector.subscribe(".*\\..*");
            connector.subscribe("bigdata.t_user");
            connector.rollback();
            int totalEmptyCount = 10 * _60SECONDS;
            while (emptyCount < totalEmptyCount) {
                System.out.println("我是canal,每秒一次正在监听:"+ UUID.randomUUID().toString());
                Message message = connector.getWithoutAck(batchSize); // 获取指定数量的数据
                long batchId = message.getId();
                int size = message.getEntries().size();
                if (batchId == -1 || size == 0) {
                    emptyCount++;
                    try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
                } else {
                    //计数器重新置零
                    emptyCount = 0;
                    printEntry(message.getEntries());
                }
                connector.ack(batchId); // 提交确认
                // connector.rollback(batchId); // 处理失败, 回滚数据
            }
            System.out.println("已经监听了"+totalEmptyCount+"秒,无任何消息,请重启重试......");
        } finally {
            connector.disconnect();
        }
    }
}
  • 测试
    在这里插入图片描述
    在这里插入图片描述

在这里插入图片描述

  • 补充
    java程序下connector.subscribe配置的过滤正则
    在这里插入图片描述
    关闭资源代码简写
    在这里插入图片描述

案例落地实战bitmap/hyperloglog/GEO

统计的类型有哪些

聚合统计

统计多个集合元素的聚合结果,就是交差并等集合统计。
在这里插入图片描述

排序统计

抖音短视频最新评论留言的场景,请你设计一个展现列表。考察你的数据结构和设计思路
在这里插入图片描述
在面对需要展示最新列表、排行榜等场景时,
如果数据更新频繁或者需要分页显示,建议使用ZSet

二值统计

集合元素的取值就只有O和1两种。
在钉钉上班签到打卡的场景中,我们只用记录有签到(⑴或没签到(O)
使用bitmap

基数统计

指统计一个集合中不重复的元素个数
使用hyperloglog

hyperloglog

名词解释
  • UV(Unique Visitor)
    独立访客,一般解释为客户端ip,需要考虑去重
  • PV(Page View)
    页面游览量,不用去重
  • DAU(Daily Active User)
    日活跃用户量,登录或者使用了某个产品的用户数(去重复登录的用户)
    常用于反映网站、互联网应用或者网络游戏的运营情况
  • MAU(Monthly Active User)
    月活跃用户量
需求分析

很多计数类场景,比如 每日注册 IP 数、每日访问 IP 数、页面实时访问数 PV、访问用户数 UV等。
因为主要的目标高效、巨量地进行计数,所以对存储的数据的内容并不太关心。
也就是说它只能用于统计巨量数量,不太涉及具体的统计对象的内容和精准性。
统计单日一个页面的访问量(PV),单次访问就算一次。
统计单日一个页面的用户访问量(UV),即按照用户为维度计算,单个用户一天内多次访问也只算一次。
多个key的合并统计,某个门户网站的所有模块的PV聚合统计就是整个网站的总PV。

hyperloglog是什么

基数:是一种数据集,去重复后的真实个数
在这里插入图片描述
在这里插入图片描述
基数统计: 用于统计一个集合中不重复的元素个数,就是对集合去重复后剩余元素的计算
在这里插入图片描述

亿级的Redis统计方案
  • 需求

UV的统计需要去重,一个用户一天内的多次访问只能算作一次
淘宝、天猫首页的UV,平均每天是1~1.5个亿左右
每天存1.5个亿的IP,访问者来了后先去查是否存在,不存在加入

  • 方案讨论
  • 用redis的hash结构存储
    在这里插入图片描述
    redis——hash = <keyDay,<ip,1>>
    按照ipv4的结构来说明,每个ipv4的地址最多是15个字节(ip = “192.168.111.1”,最多xxx.xxx.xxx.xxx)
    1.5亿 * 15个字节= 2G,一个月60G,redis就挂了
  • hyperloglog
    在这里插入图片描述
    在这里插入图片描述
  • service
@Service
@Slf4j
public class HyperLogLogService
{
    @Resource
    private RedisTemplate redisTemplate;

    /**
     * 模拟后台有用户点击首页,每个用户来自不同ip地址
     */
    @PostConstruct
    public void init()
    {
        log.info("------模拟后台有用户点击首页,每个用户来自不同ip地址");
        new Thread(() -> {
            String ip = null;
            for (int i = 1; i <=200; i++) {
                Random r = new Random();
                ip = r.nextInt(256) + "." + r.nextInt(256) + "." + r.nextInt(256) + "." + r.nextInt(256);

                Long hll = redisTemplate.opsForHyperLogLog().add("hll", ip);
                log.info("ip={},该ip地址访问首页的次数={}",ip,hll);
                //暂停几秒钟线程
                try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
            }
        },"t1").start();
    }

}
  • controller
@Api(description = "淘宝亿级UV的Redis统计方案")
@RestController
@Slf4j
public class HyperLogLogController
{
    @Resource
    private RedisTemplate redisTemplate;

    @ApiOperation("获得IP去重后的首页访问量")
    @RequestMapping(value = "/uv",method = RequestMethod.GET)
    public long uv()
    {
        //pfcount
        return redisTemplate.opsForHyperLogLog().size("hll");
    }

}
  • 测试

在这里插入图片描述
在这里插入图片描述

GEO

GEO命令复习

在这里插入图片描述
在这里插入图片描述
GEOADD city 116.403963 39.915119 "天安门" 116.403414 39.924091 "故宫" 116.024067 40.362639 "长城"

在这里插入图片描述
在这里插入图片描述
GEOPOS city 天安门 故宫

在这里插入图片描述
在这里插入图片描述
GEOHASH city 天安门 故宫 长城

在这里插入图片描述
在这里插入图片描述
GEODIST city 天安门 长城 km

georadius 以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。
GEORADIUS city 116.418017 39.914402 10 km withdist withcoord count 10 withhash desc
GEORADIUS city 116.418017 39.914402 10 km withdist withcoord count 10 desc
WITHDIST: 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。 距离的单位和用户给定的范围单位保持一致。
WITHCOORD: 将位置元素的经度和维度也一并返回。
WITHHASH: 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大
COUNT 限定返回的记录数。
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

编码实现
  • 关键点

在这里插入图片描述

  • service
@Service
@Slf4j
public class GeoService
{
    public static final String CITY ="city";

    @Autowired
    private RedisTemplate redisTemplate;

    public String geoAdd()
    {
        Map<String, Point> map= new HashMap<>();
        map.put("天安门",new Point(116.403963,39.915119));
        map.put("故宫",new Point(116.403414 ,39.924091));
        map.put("长城" ,new Point(116.024067,40.362639));

        redisTemplate.opsForGeo().add(CITY,map);

        return map.toString();
    }

    public Point position(String member) {
        //获取经纬度坐标
        List<Point> list= this.redisTemplate.opsForGeo().position(CITY,member);
        return list.get(0);
    }


    public String hash(String member) {
        //geohash算法生成的base32编码值
        List<String> list= this.redisTemplate.opsForGeo().hash(CITY,member);
        return list.get(0);
    }


    public Distance distance(String member1, String member2) {
        //获取两个给定位置之间的距离
        Distance distance= this.redisTemplate.opsForGeo().distance(CITY,member1,member2, RedisGeoCommands.DistanceUnit.KILOMETERS);
        return distance;
    }

    public GeoResults radiusByxy() {
        //通过经度,纬度查找附近的,北京王府井位置116.418017,39.914402
        Circle circle = new Circle(116.418017, 39.914402, Metrics.KILOMETERS.getMultiplier());
        //返回50条
        RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortAscending().limit(50);
        GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults= this.redisTemplate.opsForGeo().radius(CITY,circle, args);
        return geoResults;
    }

    public GeoResults radiusByMember() {
        //通过地方查找附近
        String member="天安门";
        //返回50条
        RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortAscending().limit(50);
        //半径10公里内
        Distance distance=new Distance(10, Metrics.KILOMETERS);
        GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults= this.redisTemplate.opsForGeo().radius(CITY,member, distance,args);
        return geoResults;
    }
}
  • controller
@Api(tags = "美团地图位置附近的酒店推送GEO")
@RestController
@Slf4j
public class GeoController
{
    @Resource
    private GeoService geoService;

    @ApiOperation("添加坐标geoadd")
    @RequestMapping(value = "/geoadd",method = RequestMethod.GET)
    public String geoAdd()
    {
        return geoService.geoAdd();
    }

    @ApiOperation("获取经纬度坐标geopos")
    @RequestMapping(value = "/geopos",method = RequestMethod.GET)
    public Point position(String member)
    {
        return geoService.position(member);
    }

    @ApiOperation("获取经纬度生成的base32编码值geohash")
    @RequestMapping(value = "/geohash",method = RequestMethod.GET)
    public String hash(String member)
    {
        return geoService.hash(member);
    }

    @ApiOperation("获取两个给定位置之间的距离")
    @RequestMapping(value = "/geodist",method = RequestMethod.GET)
    public Distance distance(String member1, String member2)
    {
        return geoService.distance(member1,member2);
    }

    @ApiOperation("通过经度纬度查找北京王府井附近的")
    @RequestMapping(value = "/georadius",method = RequestMethod.GET)
    public GeoResults radiusByxy()
    {
        return geoService.radiusByxy();
    }

    @ApiOperation("通过地方查找附近,本例写死天安门作为地址")
    @RequestMapping(value = "/georadiusByMember",method = RequestMethod.GET)
    public GeoResults radiusByMember()
    {
        return geoService.radiusByMember();
    }

}
  • 测试
  • 添加坐标geoadd在这里插入图片描述
  • 获取经纬度坐标geopos
    在这里插入图片描述
  • 获取经纬度生成的base32编码值geohash
    在这里插入图片描述
  • 获取两个给定位置之间的距离
    在这里插入图片描述
  • 通过经度纬度查找北京王府井附近的
    在这里插入图片描述
  • 通过地方查找附近,本例写死天安门作为地址
    在这里插入图片描述

bitmap

命令复习
  • setbit

在这里插入图片描述
setbit键偏移位只能零或者1
Bitmap的偏移量是从零开始算的

  • gitbit

getbit key offset

  • setbit和gitbit案例说明

按照天:
在这里插入图片描述

  • bitmap的底层编码说明,get命令操作如何

实质是二进制的ascii编码对应
在这里插入图片描述

  • strlen

统计字节数占用多少
在这里插入图片描述
不是字符串长度而是占据几个字节,超过8位后自己按照8位一组一byte再扩容

  • bitcount

全部键里面含有1的有多少个
在这里插入图片描述

布隆过滤器BloomFilter

是什么

由一个初值都为零的bit数组和多个哈希函数构成,用来快速判断集合中是否存在某个元素
在这里插入图片描述

目的方式
减少内存占用不保存数据信息,只是在内存中做一个是否存在的标记flag

本质就是判断具体数据是否存在于一个大的集合中
它实际上是一个很长的二进制数组(00000000)+一系列随机hash算法映射函数,主要用于判断一个元素是否在集合中。
通常我们会遇到很多要判断一个元素是否在某个集合中的业务场景,一般想到的是将集合中所有元素保存起来,然后通过比较确定。
链表、树、哈希表等等数据结构都是这种思路。但是随着集合中元素的增加,我们需要的存储空间也会呈现线性增长,最终达到瓶颈。同时检索速度也越来越慢,上述三种结构的检索时间复杂度分别为O(n),O(logn),O(1)。这个时候,布隆过滤器(Bloom Filter)就应运而生
在这里插入图片描述

能干吗

高效地插入和查询,占用空间少,返回的结果是不确定性+不够完美。
一个元素如果判断结果:存在时,元素不一定存在,
但是判断结果为不存在时,则一定不存在。

布隆过滤器可以添加元素,但是不能删除元素
由于涉及hashcode判断依据,删掉元素会导致误判率增加。
可以保证的是,
如果布隆过滤器判断一个元素不在一个集合中,那这个元素一定不会在集合中

原理

  • 布隆过滤器实现原理和数据结构

布隆过滤器(Bloom Filter) 是一种专门用来解决去重问题的高级数据结构。
实质就是一个大型位数组和几个不同的无偏hash函数(无偏表示分布均匀)。由一个初值都为零的bit数组和多个个哈希函数构成,用来快速判断某个数据是否存在。但是跟 HyperLogLog 一样,它也一样有那么一点点不精确,也存在一定的误判概率
添加key时:
使用多个hash函数对key进行hash运算得到一个整数索引值,对位数组长度进行取模运算得到一个位置,
每个hash函数都会得到一个不同的位置,将这几个位置都置1就完成了add操作。
查询key时:
只要有其中一位是零就表示这个key不存在,但如果都是1,则不一定存在对应的key

hash冲突导致数据不精准
当有变量被加入集合时,通过N个映射函数将这个变量映射成位图中的N个点,
把它们置为 1(假定有两个变量都通过 3 个映射函数)。
在这里插入图片描述
查询某个变量的时候我们只要看看这些点是不是都是 1, 就可以大概率知道集合中有没有它了
如果这些点,有任何一个为零则被查询变量一定不在
如果都是 1,则被查询变量很可能存在
为什么说是可能存在,而不是一定存在呢?那是因为映射函数本身就是散列函数,散列函数是会有碰撞的。(见上图3号坑两个对象都1)
在这里插入图片描述
哈希函数的概念是:将任意大小的输入数据转换成特定大小的输出数据的函数,转换后的数据称为哈希值或哈希编码,也叫散列值
在这里插入图片描述
如果两个散列值是不相同的(根据同一函数)那么这两个散列值的原始输入也是不相同的。
这个特性是散列函数具有确定性的结果,具有这种性质的散列函数称为单向散列函数。
散列函数的输入和输出不是唯一对应关系的,如果两个散列值相同,两个输入值很可能是相同的,但也可能不同,
这种情况称为“散列碰撞(collision)”。
用 hash表存储大数据量时,空间效率还是很低,当只有一个 hash 函数时,还很容易发生哈希碰撞。

  • 使用3步骤
  • 初始化bitmap
    布隆过滤器 本质上 是由长度为 m 的位向量或位列表(仅包含 0 或 1 位值的列表)组成,最初所有的值均设置为 0
    在这里插入图片描述
  • 添加占坑位
    当我们向布隆过滤器中添加数据时,为了尽量地址不冲突,会使用多个 hash 函数对 key 进行运算,算得一个下标索引值,然后对位数组长度进行取模运算得到一个位置,每个 hash 函数都会算得一个不同的位置。再把位数组的这几个位置都置为 1 就完成了 add 操作。
    例如,我们添加一个字符串wmyskxz,对字符串进行多次hash(key) → 取模运行→ 得到坑位
    在这里插入图片描述
  • 判断是否存在
    向布隆过滤器查询某个key是否存在时,先把这个 key 通过相同的多个 hash 函数进行运算,查看对应的位置是否都为 1,
    只要有一个位为零,那么说明布隆过滤器中这个 key 不存在;
    如果这几个位置全都是 1,那么说明极有可能存在;
    就比如我们在 add 了字符串wmyskxz数据之后,很明显下面1/3/5 这几个位置的 1 是因为第一次添加的 wmyskxz 而导致的;
    此时我们查询一个没添加过的不存在的字符串inexistent-key,它有可能计算后坑位也是1/3/5 ,这就是误判了
    在这里插入图片描述
  • 布隆过滤器误判率,为什么不要删除

布隆过滤器的误判是指多个输入经过哈希之后在相同的bit位置1了,这样就无法判断究竟是哪个输入产生的,
因此误判的根源在于相同的 bit 位被多次映射且置 1。
这种情况也造成了布隆过滤器的删除问题,因为布隆过滤器的每一个 bit 并不是独占的,很有可能多个元素共享了某一位
如果我们直接删除这一位的话,会影响其他的元素
布隆过滤器可以添加元素,但是不能删除元素。因为删掉元素会导致误判率增加。

  • 总结

有,很可能有,没有,绝对没有
使用时最好不要让实际元素数量远大于初始化数量,一次给够避免扩容
当实际元素数量超过初始化数量时,应该对布隆过滤器进行重建,重新分配一个size更大的过滤器,再将所有的历史元素批量add进行

使用场景(缓存穿透)

  • 缓存穿透是什么

一般情况下,先查询缓存redis是否有该条数据,缓存中没有时,再查询数据库。
当数据库也不存在该条数据时,每次查询都要访问数据库,这就是缓存穿透。
缓存透带来的问题是,当有大量请求查询数据库不存在的数据时,就会给数据库带来压力,甚至会拖垮数据库。

  • 可以使用布隆过滤器解决缓存穿透的问题

把已存在数据的key存在布隆过滤器中,相当于redis前面挡着一个布隆过滤器。
当有新的请求时,先到布隆过滤器中查询是否存在:
如果布隆过滤器中不存在该条数据则直接返回;
如果布隆过滤器中已存在,才去查询缓存redis,如果redis里没查询到则再查询Mysql数据库
在这里插入图片描述

手写简单的布隆过滤器(结合bitmap)

整体架构

在这里插入图片描述

步骤设计
  • redis的setbit/getbit

在这里插入图片描述

  • setbit的构建过程

@PostConstruct初始化白名单数据
计算元素的hash值
通过上一步hash值算出对应的二进制数组的坑位
将对应坑位的值的修改为数字1,表示存在

  • gitbit查询是否存在

计算元素的hash值
通过上一步hash值算出对应的二进制数组的坑位
返回对应坑位的值,零表示无,1表示存在

Springboot+redis+mybatis案例基础
  • 用户表sql
CREATE TABLE `t_customer` (

  `id` int(20) NOT NULL AUTO_INCREMENT,

  `cname` varchar(50) NOT NULL,

  `age` int(10) NOT NULL,

  `phone` varchar(20) NOT NULL,

  `sex` tinyint(4) NOT NULL,

  `birth` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

  PRIMARY KEY (`id`),

  KEY `idx_cname` (`cname`)

) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4
  • pom
	    <!--Mysql数据库驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
        <!--SpringBoot集成druid连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <!--MybatisPlus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.3.1</version>
        </dependency>
  • yml
 # ========================alibaba.druid=====================
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: 123456
    druid:
      test-while-idle: false
# ========mybatisplus===========
mybatis-plus:
  global-config:
    db-config:
      id-type: auto
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath:mapper/*.xml
  • 主启动
@SpringBootApplication
@MapperScan("com.zdz.mapper")
public class Redis7Study7777 {
    public static void main(String[] args) {
        SpringApplication.run(Redis7Study7777.class, args);
    }
}
  • service
@Service
@Slf4j
public class CustomerServiceImpl extends ServiceImpl<CustomerMapper, Customer>
    implements CustomerService{
    public static final String CACHE_KEY_CUSTOMER = "customer:";

    @Resource
    private CustomerMapper customerMapper;
    @Resource
    private RedisTemplate redisTemplate;


    @Override
    public void saveCustomer(Customer customer) {
        if (customer == null) {
            return;
        }
        int i = customerMapper.insert(customer);

        if (i > 0) {
            //到数据库里面,重新捞出新数据出来,做缓存
            customer = customerMapper.selectById(customer.getId());
            //缓存key
            String key = CACHE_KEY_CUSTOMER + customer.getId();
            //往mysql里面插入成功随后再从mysql查询出来,再插入redis
            redisTemplate.opsForValue().set(key, customer);
        }
    }

    @Override
    public Customer getCustomerById(Integer id) {
        Customer customer = null;
        //缓存key的名称
        String key = CACHE_KEY_CUSTOMER + id;
        //1 查询redis
        customer = (Customer) redisTemplate.opsForValue().get(key);
        //redis无,进一步查询mysql
        if (customer == null) {
            //2 从mysql查出来customer
            customer = customerMapper.selectById(id);
            // mysql有,redis无
            if (customer != null) {
                //3 把mysql捞到的数据写入redis,方便下次查询能redis命中。
                redisTemplate.opsForValue().set(key, customer);
            }
        }
        return null;
    }
}

  • controller
@RestController
@Api("客户Customer接口+布隆过滤器讲解")
@Slf4j
public class CustomerController {
    @Resource
    private CustomerService customerService;

    @ApiOperation("添加顾客")
    @PostMapping("/customer")
    public void saveCustomer(@RequestBody Customer customer) {
        log.info("添加顾客: {}", customer);
        customerService.saveCustomer(customer);
    }

    @ApiOperation("单个用户查询,按customerid查用户信息")
    @GetMapping("/customer/{id}")
    public Customer getCustomerById(@PathVariable Integer id) {
        log.info("单个用户查询,按customerid查用户信息: {}", id);
        return customerService.getCustomerById(id);
    }
}
新增布隆过滤器
  • BooleanFilterInit(白名单)
@Component
@Slf4j
public class BloomFilterInit {
    @Resource
    private RedisTemplate redisTemplate;

    @PostConstruct//初始化白名单数据,故意差异化数据演示效果......
    public void init() {
        //白名单客户预加载到布隆过滤器
        Set<String> keys = redisTemplate.keys(CustomerServiceImpl.CACHE_KEY_CUSTOMER + "*");
        for (String key : keys) {
            //1 计算hashcode,由于可能有负数,直接取绝对值
            int hashValue = Math.abs(key.hashCode());
            //2 通过hashValue和2的32次方取余后,获得对应的下标坑位
            long index = (long) (hashValue % Math.pow(2, 32));
            log.info(key+" 对应------坑位index:{}",index);
            //3 设置redis里面bitmap对应坑位,该有值设置为1
            redisTemplate.opsForValue().setBit("whitelistCustomer", index, true);
        }

    }
}
  • CheckUtils
@Component
@Slf4j
public class CheckUtils {
    @Resource
    public RedisTemplate redisTemplate;

    public boolean checkWithBloomFilter(String checkItem, String key) {
        int hashValue = Math.abs(key.hashCode());
        long index = (long) (hashValue % Math.pow(2, 32));
        Boolean isExist = redisTemplate.opsForValue().getBit(checkItem, index);
        log.info("----->key:"+key+"\t对应坑位index:"+index+"\t是否存在:"+isExist);
        return isExist;
    }
}
  • service新加方法
@Resource
private CheckUtils checkUtils;
public Customer getCustomerByIdWithBloomFilter(Integer id) {
        Customer customer = null;

        //缓存key的名称
        String key = CACHE_KEY_CUSTOMER + id;

        //布隆过滤器check,无是绝对无,有是可能有
        if (!checkUtils.checkWithBloomFilter("whitelistCustomer", key)) {
            log.info("白名单无此顾客信息:{}",key);
            return null;
        }
        //1 查询redis
        customer = (Customer) redisTemplate.opsForValue().get(key);
        //redis无,进一步查询mysql
        if (customer == null) {
            //2 从mysql查出来customer
            customer = customerMapper.selectById(id);
            // mysql有,redis无
            if (customer != null) {
                //3 把mysql捞到的数据写入redis,方便下次查询能redis命中。
                redisTemplate.opsForValue().set(key, customer);
            }
        }
        return customer;
    }
  • controller新增方法
	@ApiOperation("BloomFilter案例讲解")
    @GetMapping("/customerBloomFilter/{id}")
    public Customer getCustomerByIdWithBloomFilter(@PathVariable Integer id){
        log.info("单个用户查询,按customerid查用户信息: {}", id);
        return customerService.getCustomerByIdWithBloomFilter(id);
    }
  • 测试

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

优缺点

  • 优点

高效地插入和查询,内存占用bit空间少

  • 缺点

不能删除元素。
因为删掉元素会导致误判率增加,因为hash冲突同一个位置可能存的东西是多个共有的,你删除一个元素的同时可能也把其它的删除了。
存在误判,不能精准过滤

缓存预热+缓存雪崩+缓存击穿+缓存穿透

缓存预热

@PostConstruct初始化白名单数据

缓存雪崩

发生
redis主机挂了,Redis 全盘崩溃,偏硬件运维
redis中有大量key同时过期大面积失效,偏软件开发

预防+解决

  • redis中key设置为永不过期or 过期时间错开
  • redis缓存集群实现高可用
    主从+哨兵
    开启Redis持久化机制aof/rdb,尽快恢复缓存集群
    Redis Cluster
  • 多缓存结合硕防雪崩
    ehcache本地缓存+redis缓存
  • 服务降级
    Hystrix或者阿里sentinel限流&降级

缓存穿透

是什么

请求去查询一条记录,先查redis无,后查mysql无,都查询不到该条记录,
但是请求每次都会打到数据库上面去,导致后台数据库压力暴增,
这种现象我们称为缓存穿透,这个redis变成了一个摆设。
简单说就是
本来无一物,两库都没有。
既不在Redis缓存库,也不在mysql,数据库存在被多次暴击风险

解决

在这里插入图片描述

  • 空对象缓存或者缺省值

如果发生了缓存穿透,我们可以针对要查询的数据,在Redis里存一个和业务部门商量后确定的缺省值(比如,零、负数、defaultNull等)。
比如,键uid:abcdxxx,值defaultNull作为案例的key和value
先去redis查键uid:abcdxxx没有,再去mysql查没有获得 ,这就发生了一次穿透现象。
but,可以增强回写机制
mysql也查不到的话也让redis存入刚刚查不到的key并保护mysql。
第一次来查询uid:abcdxxx,redis和mysql都没有,返回null给调用者,但是增强回写后第二次来查uid:abcdxxx,此时redis就有值了。
可以直接从Redis中读取default缺省值返回给业务应用程序,避免了把大量请求发送给mysql处理,打爆mysql。
但是抵挡不住黑客的攻击如果攻击的key不同

  • Google布隆过滤器Guava解决缓存穿透(白名单过滤器)

在这里插入图片描述
误判问题,但是概率小可以接受,不能从布隆过滤器删除
全部合法的key都需要放入Guava版布隆过滤器+redis里面,不然数据就是返回null

  • pom
		<!--guava Google 开源的 Guava 中自带的布隆过滤器-->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>23.0</version>
        </dependency>
  • service
@Service
@Slf4j
public class GuavaBloomFilterService {
    public static final int _1W = 10000;
    // 布隆过滤器里预计要出入多少数据
    public static int size = 100 * _1W;
    // 误判率,它越小误判的个数也就越少
    public static double fpp = 0.03;
    // 构建布隆过滤器
    private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size, fpp);

    public void guavaBloomFilter() {
        //1. 先往布隆过滤器里面插入100万的样本数据
        for (int i = 0; i < size; i++) {
            bloomFilter.put(i);
        }

        // 故意取10万个不在过滤器里的值,看看有多少个会被认为在过滤器里
        List<Integer> list = new ArrayList<>(10 * _1W);
        for (int i = size + 1; i < size + (10 * _1W); i++) {
            if (bloomFilter.mightContain(i)) {
                log.info("被误判了: {}", i);
                list.add(i);
            }
        }
        log.info("误判的总数量: {}", list.size());
    }
}
  • controller
@Api(tags = "google工具Guava处理布隆过滤器")
@RestController
@Slf4j
public class GuavaBloomFilterController
{
    @Resource
    private GuavaBloomFilterService guavaBloomFilterService;

    @ApiOperation("guava布隆过滤器插入100万样本数据并额外10W测试是否存在")
    @RequestMapping(value = "/guavafilter",method = RequestMethod.GET)
    public void guavaBloomFilter()
    {
        guavaBloomFilterService.guavaBloomFilter();
    }
}
  • 测试

在这里插入图片描述
在这里插入图片描述

  • 布隆过滤器说明

在这里插入图片描述

缓存击穿

是什么

大量的请求同时查询一个key时,
此时这个key正好失效了,就会导致大量的请求都打到数据库上面去
简单说就是热点key突然失效了,暴打mysql

解决

热点key失效
时间到了自然清除但还被访问到
delete掉的key,刚巧又被访问

  • 方案1

差异失效时间,对于访问频繁的热点key,干脆就不设置过期时间

  • 方案2

互斥跟新,采用双检加锁策略
多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个 互斥锁来锁住它。
其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有缓存了,就直接走缓存。
在这里插入图片描述

案例(模拟高并发的天猫聚划算案例code)
  • entity
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(value = "聚划算活动producet信息")
public class Product
{
    //产品ID
    private Long id;
    //产品名称
    private String name;
    //产品价格
    private Integer price;
    //产品详情
    private String detail;
}

  • service
@Service
@Slf4j
public class JHSTaskService {
    private static final String JHS_KEY="jhs";
    private static final String JHS_KEY_A="jhs:a";
    private static final String JHS_KEY_B="jhs:b";

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 模拟从数据库读取100件特价商品,用于加载到聚划算的页面中
     * @return
     */
    private List<Product> getProductsFromMysql() {
        ArrayList<Product> list = new ArrayList<>();
        for (int i = 1; i <= 20; i++) {
            Random random = new Random();
            int id = random.nextInt(10000);
            Product product = new Product((long) id, "product" + i, i, "detail");
            list.add(product);
        }
        return list;
    }

    @PostConstruct
    public void initJHS() {
        log.info("启动定时器淘宝聚划算功能模拟................" + DateUtil.now());
        new Thread(() -> {
            //模拟定时器一个后台任务,定时把数据库的特价商品,刷新到redis中
            while (true) {
                //模拟从数据库读取100件特价商品,用于加载到聚划算的页面中
                List<Product> list = getProductsFromMysql();
                //采用redis list数据结构的lpush来实现存储
                redisTemplate.delete(JHS_KEY);
                //lpush命令
                redisTemplate.opsForList().leftPushAll(JHS_KEY, list);
                //间隔一分钟 执行一遍,模拟聚划算每3天刷新一批次参加活动
                try {TimeUnit.SECONDS.sleep(30);} catch (InterruptedException e) {e.printStackTrace();}
                log.info("runJHS定时刷新");
            }
        }, "t1").start();
    }
}
  • controller
@RestController
@Slf4j
@Api(tags = "聚划算商品列表接口")
public class JHSProductController
{
    public  static final String JHS_KEY="jhs";
    public  static final String JHS_KEY_A="jhs:a";
    public  static final String JHS_KEY_B="jhs:b";

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 分页查询:在高并发的情况下,只能走redis查询,走db的话必定会把db打垮
     * @param page
     * @param size
     * @return
     */
    @RequestMapping(value = "/pruduct/find",method = RequestMethod.GET)
    @ApiOperation("按照分页和每页显示容量,点击查看")
    public List<Product> find(int page, int size) {
        List<Product> list=null;

        long start = (page - 1) * size;
        long end = start + size - 1;

        try {
            //采用redis list数据结构的lrange命令实现分页查询
            list = this.redisTemplate.opsForList().range(JHS_KEY, start, end);
            if (CollectionUtils.isEmpty(list)) {
                //TODO 走DB查询
            }
            log.info("查询结果:{}", list);
        } catch (Exception ex) {
            //这里的异常,一般是redis瘫痪 ,或 redis网络timeout
            log.error("exception:", ex);
            //TODO 走DB查询
        }

        return list;
    }
}
  • 测试

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • bug和隐患说明

热点key突然失效导致可怕的缓存击穿
delete命令执行的一瞬间有空隙,其它请求线程继续找Redis为null
打到了mysql
在这里插入图片描述

  • 进一步升级加固案例

差异失效时间
在这里插入图片描述
service

@PostConstruct
   public void initJHSAB(){
       log.info("启动AB定时器计划任务淘宝聚划算功能模拟.........."+DateUtil.now());
       new Thread(() -> {
           //模拟定时器,定时把数据库的特价商品,刷新到redis中
           while (true){
               //模拟从数据库读取100件特价商品,用于加载到聚划算的页面中
               List<Product> list=this.getProductsFromMysql();
              //先更新B缓存
               this.redisTemplate.delete(JHS_KEY_B);
               this.redisTemplate.opsForList().leftPushAll(JHS_KEY_B,list);
               this.redisTemplate.expire(JHS_KEY_B,20L,TimeUnit.DAYS);
               //再更新A缓存
               this.redisTemplate.delete(JHS_KEY_A);
               this.redisTemplate.opsForList().leftPushAll(JHS_KEY_A,list);
               this.redisTemplate.expire(JHS_KEY_A,15L,TimeUnit.DAYS);
               //间隔一分钟 执行一遍
               try { TimeUnit.MINUTES.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }

               log.info("runJhs定时刷新双缓存AB两层..............");
           }
       },"t1").start();
   }

controller

@RequestMapping(value = "/pruduct/findab",method = RequestMethod.GET)
   @ApiOperation("防止热点key突然失效,AB双缓存架构")
   public List<Product> findAB(int page, int size) {
       List<Product> list=null;
       long start = (page - 1) * size;
       long end = start + size - 1;
       try {
           //采用redis list数据结构的lrange命令实现分页查询
           list = this.redisTemplate.opsForList().range(JHS_KEY_A, start, end);
           if (CollectionUtils.isEmpty(list)) {
               log.info("=========A缓存已经失效了,记得人工修补,B缓存自动延续5天");
               //用户先查询缓存A(上面的代码),如果缓存A查询不到(例如,更新缓存的时候删除了),再查询缓存B
               this.redisTemplate.opsForList().range(JHS_KEY_B, start, end);
               //TODO 走DB查询
           }
           log.info("查询结果:{}", list);
       } catch (Exception ex) {
           //这里的异常,一般是redis瘫痪 ,或 redis网络timeout
           log.error("exception:", ex);
           //TODO 走DB查询
       }
       return list;
   }

总结

在这里插入图片描述

Redlock算法和底层源码分析

Redis分布式锁-Redlock红锁算法

为什么学习这个

在这里插入图片描述
在这里插入图片描述
线程 1 首先获取锁成功,将键值对写入 redis 的 master 节点,在 redis 将该键值对同步到 slave 节点之前,master 发生了故障;redis 触发故障转移,其中一个 slave 升级为新的 master,此时新上位的master并不包含线程1写入的键值对,因此线程 2 尝试获取锁也可以成功拿到锁,此时相当于有两个线程获取到了锁,可能会导致各种预期之外的情况发生,例如最常见的脏数据。
我们加的是排它独占锁,同一时间只能有一个建redis锁成功并持有锁,严禁出现2个以上的请求线程拿到锁。危险的

Redlock算法设计理念
  • redis之父提出了Redlock算法解决这个问题

Redis也提供了Redlock算法,用来实现基于多个实例的分布式锁。
锁变量由多个实例维护,即使有实例发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作。
Redlock算法是实现高可靠分布式锁的一种有效解决方案,可以在实际开发中使用。
在这里插入图片描述

  • 设计理念

该方案也是基于(set 加锁、Lua 脚本解锁)进行改良的,所以redis之父antirez 只描述了差异的地方,大致方案如下。
假设我们有N个Redis主节点,例如 N = 5这些节点是完全独立的,我们不使用复制或任何其他隐式协调系统,
为了取到锁客户端执行以下操作:

1获取当前时间,以毫秒为单位;
2依次尝试从5个实例,使用相同的 key 和随机值(例如 UUID)获取锁。当向Redis 请求获取锁时,客户端应该设置一个超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为 10 秒,则超时时间应该在 5-50 毫秒之间。这样可以防止客户端在试图与一个宕机的 Redis 节点对话时长时间处于阻塞状态。如果一个实例不可用,客户端应该尽快尝试去另外一个 Redis 实例请求获取锁;
3客户端通过当前时间减去步骤 1 记录的时间来计算获取锁使用的时间。当且仅当从大多数(N/2+1,这里是 3 个节点)的 Redis 节点都取到锁,并且获取锁使用的时间小于锁失效时间时,锁才算获取成功;
4如果取到了锁,其真正有效时间等于初始有效时间减去获取锁所使用的时间(步骤 3 计算的结果)。
5如果由于某些原因未能获得锁(无法在至少 N/2 + 1 个 Redis 实例获取锁、或获取锁的时间超过了有效时间),客户端应该在所有的 Redis 实例上进行解锁(即便某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。

该方案为了解决数据不一致的问题,直接舍弃了异步复制只使用 master 节点,同时由于舍弃了 slave,为了保证可用性,引入了 N 个节点,官方建议是 5。
客户端只有在满足下面的这两个条件时,才能认为是加锁成功。
条件1:客户端从超过半数(大于等于N/2+1)的Redis实例上成功获取到了锁;
条件2:客户端获取锁的总耗时没有超过锁的有效时间。

  • 解决方案

在这里插入图片描述
为什么是奇数? N = 2X + 1 (N是最终部署机器数,X是容错机器数)

Redisson源码解析

  • 守护线程续命

额外起一个线程,定期检查线程是否还持有锁,如果有则延长过期时间。
Redisson 里面就实现了这个方案,使用“看门狗”定期检查(每1/3的锁时间检查1次),如果线程还持有锁,则刷新过期时间;

  • watchdog看门狗

在获取锁成功后,给锁加一个watchdog, watchdog
会起一个定时任务,在锁没有被释放且快要过期的时候会续期

  • key默认过期时间

在这里插入图片描述
在这里插入图片描述

  • 续命方法源码

在这里插入图片描述

  • 加锁源码

在这里插入图片描述
通过exists判断,如果锁不存在,则设置值和过期时间,加锁成功
通过hexists判断,如果锁已存在,并且锁的是当前线程,则证明是重入锁,加锁成功
如果锁已存在,但锁的不是当前线程,则证明有其他线程持有锁。返回当前锁的过期时间(代表了锁key的剩余生存时间),加锁失败

  • watchdog源码

在这里插入图片描述
这里面初始化了一个定时器,dely 的时间是 internalLockLeaseTime/3。
在 Redisson 中,internalLockLeaseTime 是 30s,也就是每隔 10s 续期一次,每次 30s
在这里插入图片描述
客户端A加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端A还持有锁key,那么就会不断的延长锁key的生存时间,默认每次续命又从30秒新开始
在这里插入图片描述

  • 解锁源码

在这里插入图片描述

多机案例

在这里插入图片描述

  • docker启动3台redis实例
  • 建Module
  • 改pom
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.19.1</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.8</version>
        </dependency>
        <!--swagger-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <!--swagger-ui-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.4</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.11</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>


    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.springframework.boot</groupId>
                            <artifactId>spring-boot-configuration-processor</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
  • yml
server.port=9090
spring.application.name=redlock


spring.swagger2.enabled=true


spring.redis.database=0
spring.redis.password=
spring.redis.timeout=3000
spring.redis.mode=single

spring.redis.pool.conn-timeout=3000
spring.redis.pool.so-timeout=3000
spring.redis.pool.size=10

spring.redis.single.address1=192.168.111.185:6381
spring.redis.single.address2=192.168.111.185:6382
spring.redis.single.address3=192.168.111.185:6383
  • 主启动
  • 配置类

CacheConfiguration

@Configuration
@EnableConfigurationProperties(RedisProperties.class)
public class CacheConfiguration {

    @Autowired
    RedisProperties redisProperties;

    @Bean
    RedissonClient redissonClient1() {
        Config config = new Config();
        String node = redisProperties.getSingle().getAddress1();
        node = node.startsWith("redis://") ? node : "redis://" + node;
        SingleServerConfig serverConfig = config.useSingleServer()
                .setAddress(node)
                .setTimeout(redisProperties.getPool().getConnTimeout())
                .setConnectionPoolSize(redisProperties.getPool().getSize())
                .setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());
        if (StringUtils.isNotBlank(redisProperties.getPassword())) {
            serverConfig.setPassword(redisProperties.getPassword());
        }
        return Redisson.create(config);
    }

    @Bean
    RedissonClient redissonClient2() {
        Config config = new Config();
        String node = redisProperties.getSingle().getAddress2();
        node = node.startsWith("redis://") ? node : "redis://" + node;
        SingleServerConfig serverConfig = config.useSingleServer()
                .setAddress(node)
                .setTimeout(redisProperties.getPool().getConnTimeout())
                .setConnectionPoolSize(redisProperties.getPool().getSize())
                .setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());
        if (StringUtils.isNotBlank(redisProperties.getPassword())) {
            serverConfig.setPassword(redisProperties.getPassword());
        }
        return Redisson.create(config);
    }

    @Bean
    RedissonClient redissonClient3() {
        Config config = new Config();
        String node = redisProperties.getSingle().getAddress3();
        node = node.startsWith("redis://") ? node : "redis://" + node;
        SingleServerConfig serverConfig = config.useSingleServer()
                .setAddress(node)
                .setTimeout(redisProperties.getPool().getConnTimeout())
                .setConnectionPoolSize(redisProperties.getPool().getSize())
                .setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());
        if (StringUtils.isNotBlank(redisProperties.getPassword())) {
            serverConfig.setPassword(redisProperties.getPassword());
        }
        return Redisson.create(config);
    }
}

RedisPoolProperties

@Data
public class RedisPoolProperties {

    private int maxIdle;

    private int minIdle;

    private int maxActive;

    private int maxWait;

    private int connTimeout;

    private int soTimeout;

    /**
     * 池大小
     */
    private  int size;

}

RedisProperties

@ConfigurationProperties(prefix = "spring.redis", ignoreUnknownFields = false)
@Data
public class RedisProperties {

    private int database;

    /**
     * 等待节点回复命令的时间。该时间从命令发送成功时开始计时
     */
    private int timeout;

    private String password;

    private String mode;

    /**
     * 池配置
     */
    private RedisPoolProperties pool;

    /**
     * 单机信息配置
     */
    private RedisSingleProperties single;

}

RedisSingleProperties

@Data
public class RedisSingleProperties {
    private  String address1;
    private  String address2;
    private  String address3;
}

 
  • controller
@RestController
@Slf4j
public class RedLockController {

    public static final String CACHE_KEY_REDLOCK = "ATGUIGU_REDLOCK";

    @Autowired
    RedissonClient redissonClient1;

    @Autowired
    RedissonClient redissonClient2;

    @Autowired
    RedissonClient redissonClient3;

    boolean isLockBoolean;

    @GetMapping(value = "/multiLock")
    public String getMultiLock() throws InterruptedException
    {
        String uuid =  IdUtil.simpleUUID();
        String uuidValue = uuid+":"+Thread.currentThread().getId();

        RLock lock1 = redissonClient1.getLock(CACHE_KEY_REDLOCK);
        RLock lock2 = redissonClient2.getLock(CACHE_KEY_REDLOCK);
        RLock lock3 = redissonClient3.getLock(CACHE_KEY_REDLOCK);

        RedissonMultiLock redLock = new RedissonMultiLock(lock1, lock2, lock3);
        redLock.lock();
        try
        {
            System.out.println(uuidValue+"\t"+"---come in biz multiLock");
            try { TimeUnit.SECONDS.sleep(30); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(uuidValue+"\t"+"---task is over multiLock");
        } catch (Exception e) {
            e.printStackTrace();
            log.error("multiLock exception ",e);
        } finally {
            redLock.unlock();
            log.info("释放分布式锁成功key:{}", CACHE_KEY_REDLOCK);
        }

        return "multiLock task is over  "+uuidValue;
    }

}

Redis的缓存过期淘汰策略

redis默认内存多少?在哪里查看?如何设置修改?

  • 查看redis最大占用内存

在这里插入图片描述

  • 默认内存可以用多少

在这里插入图片描述

  • 一般情况下如何配置

一般推荐Redis设置内存为最大物理内存的四分之三

  • 如何修改redis内存设置
  • 配置文件修改
    在这里插入图片描述
  • 命令修改
    在这里插入图片描述
  • 什么命令查看redis内存使用情况?

info memory
config get maxmemory

Redis内存满了怎么办

把最大值改成1byte在这里插入图片描述

Redis的删除策略

  • 三种删除策略
  • 立即删除
    Redis不可能时时刻刻遍历所有被设置了生存时间的key,来检测数据是否已经到达过期时间,然后对它进行删除。
    立即删除能保证内存中数据的最大新鲜度,因为它保证过期键值会在过期后马上被删除,其所占用的内存也会随之释放。但是立即删除对cpu是最不友好的。因为删除操作会占用cpu的时间,如果刚好碰上了cpu很忙的时候,比如正在做交集或排序等计算的时候,就会给cpu造成额外的压力,让CPU心累,时时需要删除
    这会产生大量的性能消耗,同时也会影响数据的读取操作。
    对CPU不友好,用处理器性能换取存储空间(拿时间换空间)
  • 惰性删除
    数据到达过期时间,不做处理。等下次访问该数据时,
    如果未过期,返回数据 ;
    发现已过期,删除,返回不存在。
    对memory不友好,用存储空间换取处理器性能(拿空间换时间)
    开启惰性淘汰,lazyfree-lazy-eviction=yes
  • 定期删除
    定期删除策略每隔一段时间执行一次删除过期键操作并通过限制删除操作执行时长和频率来减少删除操作对CPU时间的影响。
    周期性轮询redis库中的时效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删除频度
    特点1:CPU性能占用设置有峰值,检测频度可自定义设置
    特点2:内存压力不是很大,长期占用内存的冷数据会被持续清理
    总结:周期性抽查存储空间 (随机抽查,重点抽查)
    定期抽样key,判断是否过期

Redis缓存淘汰策略

  • 配置文件

在这里插入图片描述

  • lru和lfu算法的区别

在这里插入图片描述
LRU:最近最少使用页面置换算法,淘汰最长时间未被使用的页面,看页面最后一次被使用到发生调度的时间长短,首先淘汰最长时间未被使用的页面。
LFU:最近最不常用页面置换算法,淘汰一定时期内被访问次数最少的页,看一定时间段内页面被使用的频率,淘汰一定时期内被访问次数最少的页

  • 有哪些

1.noeviction:不会驱逐任何key,表示即使内存达到上限也不进行置换,所有能引起内存增加的命令都会返回error
2.alkeys-lru:对所有key使用LRU算法进行删除,优先删除掉最近最不经常使用的key,用以保存新数据
3.volatile-lru:对所有设置了过期时间的key使用LRU算法进行删除
4.alkeys-random:对所有key随机删除
5.volatile-random:对所有设置了过期时间的key随机删除
6.volatile-tt:删除马上要过期的key
7.allkeys-lfu:对所有key使用LFU算法进行删除
8.volatile-lfu:对所有设置了过期时间的key使用LFU算法进行删除

  • redis缓存淘汰策略配置性能建议

避免存储bigkey
开启惰性淘汰,lazyfree-lazy-eviction=yes

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值