本博客参考了韩顺平老师的 Redis 课程讲义!

1 Redis 基础知识

1.1 相关资料

1.1.1 官网: https://redis.io

1.1.2下载地址: https://redis.io/docs/getting-started/installation/install-redis-on-mac-os/

1.2 为什么需要 Redis

1.2.1 企业需求

  • 高并发
  • 高可用
  • 高性能
  • 海量用户

1.2.2 关系型数据库(如 MySQL)-问题

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

1.2.3 Redis 的优势

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

1.3 Redis 简介

1.3.1 简述:

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

1.3.2 特征

  • 数据间没有必然的关联关系
  • 高性能——官方提供测试数据, 50 个并发执行 100000 个请求,读的速度是 110000 次/s,写的速度是 81000 次/s
  • 多种数据结构支持
    • String 字符串类型
    • Hash 列表类型
    • List 散列类型
    • Set 集合类型
    • SortedSet 有序集合模型
  • 持久化支持——可以进行数据灾难恢复

1.3.3 应用场景

● 为热点数据加速查询, 如热点商品、 热点新闻、 热点资讯、 推广类等高访问量信息等

● 任务队列,如秒杀、抢购、购票排队等

● 即时信息查询,如排行榜、各类网站访问统计

● 时效性信息控制,如验证码控制、投票控制等

● 分布式数据共享,如分布式集群架构中的 session 分离

● 消息队列

● 分布式锁

2 NoSQL 数据库

2.1 概述

Not-Only SQL( 泛指非关系型的数据库), 作为关系型数据库的补充

2.2 作用:

​ 应对在海量用户和海量数据的情况下, 带来的数据处理问题

2.3特点

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

SQL(关系型数据库) 🆚 NoSQL(非关系型数据库)

SQL NoSQL
数据结构 结构化(建了表就不方便再改变) 非结构化
数据关联 关联的 无关联的
查询方式 SQL语句查询 非SQL语句
事务特性 ACID BASE
存储方式 磁盘 内存
扩展性 垂直 水平
使用场景 1)数据结构固定
2)相关业务对数据安全性、一致性要求较高
1)数据结构不稳定
2)对一致性、安全性要求不高
3)对性能要求高

2.4 常见 Nosql 数据库

  • Redis
  • memcache
  • HBase
  • MongoDB

3 Redis 使用

关于本机的redis信息:

  • redis.conf 地址:/opt/homebrew/etc/redis.conf

  • redis-server地址: /opt/homebrew/opt/redis/bin/redis-server

​ 运行redis: redis-server 或 /opt/homebrew/opt/redis/bin/redis-server

停止redis:redis-cli shutdown

​ 密码设置成了 root 想要知道改了什么配置文件,请到/opt/homebrew/etc/redis.conf寻找#donn changed 20230411注释

基于配置文件运行redis:

​ redis-server /opt/homebrew/etc/redis.conf

查找redis启用的进程:

 ps axu | grep redis          

可以通过杀死进程来关闭redis服务 kill -9 进程号

redis-cli 使用:

​ redis-cli -h 127.0.0.1 -p 6379 -a root

​ -h 指定要连接redis结点的ip地址,默认为 127.0.0.1

​ -p 指定要连接的redis节点的端口,默认为 6379

​ -a 指定redis的访问密码(不建议在连接前就输入密码「不安全」)

​ —-> 建议先不输入密码,建立连接后输入 AUTH 用户名(本机未设置) 密码

mac设置redis开机自启动:

​ 1.sudo vim /Library/LaunchDaemons/redis-server.plist 创建plist文档,用于记录redis启动的配置的路径(怎么填去网上找找就行)

​ 2.运行以下命令实现开机自启动:

​ sudo launchctl load /Library/LaunchDaemons/redis-server.plist

​ (貌似是执行⬆️这条语句后,mac开机就会自动执行redis-server.plist,即自动开启redis)

​ 3.运行指令,若redis启动了即配置成功

​ sudo launchctl start redis-server (模拟重启)

图形化客户端演示

​ 在网站下载Mac版:https://redis.com/thank-you/redisinsight-the-best-redis-gui-35/ 🌟简单好用

4 Redis 指令

4.1 指令文档:

http://redis.cn/commands.html

命令十分丰富, 包括的命令组有 Cluster、 Connection、 Geo、 Hashes、 HyperLogLog、Keys、 Lists、 Pub/Sub、 Scripting、 Server、 Sets、 Sorted Sets、 Strings、 Transactions 一共 14个 redis 命令组两百多个 redis 命令

4.2 基础操作

4.2.1 通用命令

  • help 命令名称 ——> 会返回该命令的用法描述
KEYS 查看所有符合模板的key(效率低,因此不建议在生产环境设备上使用)
DEL 删除指定的key 可以一次删除多个key(用空格隔开即可)
EXISTS 判断KEY是否存在
EXPIRE Set a key’s time to live in seconds—–给一个key设置有效期,有效期到期会自动删除key
TTL Get the time to live for a key in seconds (一个key的TTL = -1表示永久有效,-2表示已过期)
type 获取key的类型
unlink 根据 value 选择非阻塞删除【 仅将 keys 从 keyspace 元数据中删除, 真正的删除会在后续异步操作】

4.2.2对 DB(数据库)操作

  • select: 命令切换数据库

     1. redis 安装后, 默认有 16 个库, 0-15
    
    1. 默认操作的是 redis 的 0 号库
  • dbsize: 查看当前数据库的 key 的数量

  • flushdb: 清空当前库

  • flushall: 清空全部库

5 Redis 五大数据类型/结构

5.1 操作文档

5.1.1 官方文档 : https://redis.io/commands

5.1.2 中文文档: http://redisdoc.com/

5.2 Redis 数据存储格式

5.2.1 一句话: redis 自身是一个 Map, 其中所有的数据都是采用 key : value 的形式存储

5.2.2 key 是字符串, value 是数据, 数据支持多种类型/结构

5.3 Redis 数据类型-5 种常用

  • string
  • hash
  • list
  • set
  • sorted_set

5.4 String类型

5.4.1 说明

  1. String 是 Redis 最基本的类型, 一个 key 对应一个 value。
  2. String 类型是二进制安全的, Redis 的 string 可以包含任何数据。 比如 jpg 图片或者序列化的对象。
  3. String 类型是 Redis 基本的数据类型, 一个 Redis 中字符串 value 最多可以是 512M

String 又可以细分为: string 普通字符串

​ int 整数类型,可以自增、自减

​ float 浮点类型,可以自增、自减

底层都是由字节数组存储,区别在于编码方式不同

  • String常见命令:
    SET SET key value
    GET GET key
    append append key value 将给定的 value 追加到原值的末尾
    (key不存在就创建)
    setnx setnx key value
    只有在 key 不存在时 设置 key 的值
    INCR(若value为int,value++) INCR key
    DECR(若value为int,value+=-1) DECR key
    INCRBY(若value为int,value+= step) INCRBY key step(step可为任意非0整数)
    INCRBYFLOAT(若value为float,value += step) INCRBYFLOAT key step ( float类型 )
    MSET(批量添加键值对) MSET key1 value1 key2 value2 …
    MGET(批量查询values) MGET key1 key2 key3 …
    msetnx msetnx
    同时设置一个或多个 key-value 对, 当且仅当所有给定 key 都不存在, 原子性, 有一个失败则都失败
    getrange getrange <起始位置><结束位置>
    获得值的范围, 类似 java 中的 substring
    两边都是闭区间,下标从0开始,负数表示从后往前数
    setrange setrange<起始位置>
    覆写所储存的字符串值, 从<起始位置>开始(索引从 0 开始)。
    SETEX setex <过期时间>
    设置键值的同时, 设置过期时间, 单位秒
    getset getset ,
    以新换旧, 设置了新值同时获得旧值
    思考:Redis中没有table的概念,那么该如何区别不同类型的key呢?(比如一个学生的id = 1,一门课程的id也为1)

    ​ Redis中 key 允许有多个单词(用于平替表的概念)形成层级结构,多个单词间用 :隔开

    ​ 格式举例—> 项目名 :业务名 :类型 :你给key起的名称

5.5 List类型(类似java 中的LinkedList)

5.5.1 概要

list 类型, 保存多个数据, 底层使用双向链表存储结构实现——(支持正向、反向检索)

5.5.2 list 存储结构示意图

​ ● 双向链表示意图

image-20230727194906350

有序、元素可重复、增删操作快、查找速度一般

● 老韩解图

1. Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。
2. 底层是个双向链表,对两端的操作性能高,通过索引下标的操作中间的节点性能较差

5.5.3 List类型常见命令:

LPUSH 向列表左侧插入一个或多个元素(一个一个push,第一个在最右边)
LPOP 移除并返回列表左侧的第一个元素,没有则返回nil
RPUSH ** ** 向列表右侧插入一个或多个元素
RPOP 移除并返回列表右侧的第一个元素,没有则返回nil
rpoplpush 列表右边吐出一个值, 插到列表左边
lrange 按照索引下标获得索引范围内的所有元素(从左到右)
lrange mylist 0 -1 0 左边第一个, -1 右边第一个, (0-1 表示获取所有)
lindex 按照索引下标获得元素(从左到右)
llen 获得列表长度
linsert before 的前面插入插入值
lrem 从左边删除 n 个 value(从左到右)
lset 将列表 key 下标为 index 的值替换成 value
BLPOP/BRPOP 与LPOP和RPOP类似,只不过在没有元素时等待指定时间,而不是直接返回nil
阻塞一段时间,在指定的时间内还没能搜索到List中对应的数据,才算是找不到

5.5.4 list 最佳实践

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

5.5.4.2 应用场景

​ ● 系统通知, 按照时间顺序展示, 将最近的通知列在前面

​ 可用于(如你发了朋友圈,记录为您点赞的用户及其点赞顺序)

5.6 Set类型(类似java 中的HashSet)

5.6.1 概要:

set 提供的功能与 list 类似是一个列表的功能, 特殊之处在于 set 是可以自动排重的, 即值是不允许重复的

​ ——无序、元素不可重复、查找快、支持交并等集合操作

​ 像是用哈希表存储的集合

5.6.2 set 常用指令&使用

5.6.2.1 set 指令操作示意图

image-20230727202023870

5.6.2.2 Set类型的常见命令:

SADD ….. 向set中添加一个或多个元素(一个key,任意多个元素),若已存在,则忽略
SMEMBERS 获取set中的所有元素
SISMEMBER 判断一个元素是否存在于set中
SCARD 返回set中元素的个数
SREM …. 移除set中的指定元素
spop 随机从该集合中吐出一个值。
srandmember 随机从该集合中取出 n 个值。 不会从集合中删除
smove value 把集合中一个值从一个集合移动到另一个集合
**SINTER **
SDIFF、SUNION…
求key1与key2的交集
差集、并集…

5.7 Hash类型(类似java 中的Map)

5.7.1 概要:

Redis hash 是一个键值对集合, hash 适合用于存储对象, 类似 Java 里面的Map<String,Object>

5.7.2 一图胜千言: Redis hash 存储结构简单示意图

image-20230727205233999

​ 无序字典,可以对对象中的每个字段独立存储,可以针对单个字段CRUD

​ 一条hash类型的记录有: 一个 key 和一些 field value

5.7.3 Hash类型常见命令:

hset 批量添加或者修改hash类型key的一个field的值
hget 获取一个hash类型key的field的值
hmset 批量添加、修改hash类型key的多个field与value
hmget 批量获取hash类型key的多个field的值
hexists 查看哈希表 key 中, 给定域 field 是否存在
hkeys 列出该 hash 集合的所有 field
hvals 列出该 hash 集合的所有 value
hincrby 为哈希表 key 中的域 field 的值加上增量 increment
hsetnx 将哈希表 key 中的域 field 的值设置为 value , 当且仅当域 field 不存在
HGETALL key 获取一个hash类型的key中的所有的field和value
HINCRBY key field 让一个hash类型key的字段值自增并指定步长
HSETNX key field value 添加一个hash类型的key的field值,前提是这个field不存在,否则不执行

5.8 SortedSet-Zset类型(功能类似java 中的TreeSet)

5.8.1 简介

  1. Redis 有序集合 zset 与普通集合 set 非常相似, 是一个没有重复元素的字符串集合
  2. 不同之处是有序集合的每个成员都关联了一个评分(score),这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。 集合的成员是唯一的, 但是评分可以是重复的 。
  3. 因为元素是有序的, 所以也可以很快的根据评分(score)或者次序(position)来获取一个范围的元素。
  4. 访问有序集合的中间元素也是非常快的, 你能够使用有序集合作为一个没有重复成员的列表。

​ ——可排序、元素不重复、查询速度快

​ 常用于实现排行榜这样的功能

5.8.2 SortedSet类型的常用命令:

zadd 添加一个或多个元素到sorted set ,如果value已经存在则更新其score值
z(rev)range [WITHSCORES] 返回有序集 key 中, 下标之间的元素,带 WITHSCORES, 可以让分数一起和值返回到结果集(默认升序、rev表示反转,即降序)
zscore 获取sorted set中的指定元素的score值
z(rev)rangebyscore key min max [withscores] 返回有序集 key 中, 所有 score 值介于min 和 max 之间(包括等于 min 或 max )的成员。 有序集成员按 score 值递增(从小到大)次序排列
zincrby 为元素的 score 加上增量
zrem 删除该集合下, 指定值的元素
zcount 统计该集合, 分数区间内的元素个数
zrank 返回该值在集合中的排名, 从 0 开始
Z(REV)RANK 获取sorted set 中的指定元素的排名(从0开始,默认升序)
ZCARD 获取sorted set中的元素个数
ZCOUNT key min max 统计score值在给定范围内的所有元素的个数
ZDIFF、ZINTER、ZUNION 求差集、交集、并集
  • 🌟注意:所有的排名默认都是升序,如果要降序则在命令的Z后面添加REV即可

6 Redis 配置

6.1 配置文档

6.1.1 参考文档 :

https://www.cnblogs.com/nhdlb/p/14048083.html#_label0

6.2 常规配置

6.2.1 设置密码

6.2.2 daemonize

6.2.3 loglevel

6.2.4 logfile

6.2.5 设定库的数量

6.3 Units 单位

如图:

image-20230728083451434

1、 配置大小单位,开头定义了一些基本的度量单位, 只支持 bytes, 不支持 bit

2、 不区分大小写

6.4 #INCLUDES#

如图:

image-20230728083845886

多实例的情况可以把公用的配置文件提取出来, 然后 include

6.5 #NETWORK#

6.5.1 bind

如图:

image-20230728084509521

  • 默认情况 bind=127.0.0.1 只能接受本机的访问请求
  • 如果服务器是需要远程访问的, 需要将其注释掉

6.5.2 protected-mode

如图:

image-20230728084720819

  • 默认是保护模式
  • 如果服务器是需要远程访问的, 需要将 yes 设置为 no

6.5.3 port

如图:

image-20230728084841445

  • Redis 服务默认端口 6379

6.5.4 timeout

如图:

image-20230728084955938

  • 一个空闲的客户端维持多少秒会关闭, 0 表示关闭该功能, 即永不超时

6.5.5 tcp-keepalive

image-20230728085057878

  • tcp-keepalive 是对访问客户端的一种心跳检测, 每隔 n 秒检测一次, 单位为秒
  • 如果设置为 0, 则不会进行 Keepalive 检测, 建议设置成 60
  • 老韩说明: 为什么需要心跳检测机制
    • TCP 协议中有长连接和短连接之分。 短连接环境下, 数据交互完毕后, 主动释放连接;
    • 长连接的环境下, 进行一次数据交互后, 很长一段时间内无数据交互时, 客户端可能意外断开, 这些 TCP 连接并未来得及正常释放, 那么, 连接的另一方并不知道对端的情况,它会一直维护这个连接, 长时间的积累会导致非常多的半打开连接, 造成端系统资源的消耗和浪费, 且有可能导致在一个无效的数据链路层面发送业务数据, 结果就是发送失败。所以服务器端要做到快速感知失败, 减少无效链接操作, 这就有了 TCP 的 Keepalive(保活探测)机制

6.6 #GENERAL 通用#

6.6.1 daemonize

如图:

image-20230728085407475

  • 是否为后台进程, 设置为 yes
  • 设置为 yes 后, 表示守护进程, 后台启动

6.6.2 pidfile

如图

image-20230728085518957

  • 存放 pid 文件的位置, 每个实例会产生一个不同的 pid 文件, 记录 redis 的进程号

6.6.3 loglevel

如图

image-20230728085914809

  • redis 日志分为 4 个级别, 默认的设置为 notice, 开发测试阶段可以用 debug(日志内容较多,不建议生产环境使用), 生产模式一般选用 notice

  • redis 日志分为 4 个级别说明

    • debug: 会打印出很多信息, 适用于开发和测试阶段;
    • verbose(冗长的): 包含很多不太有用的信息, 但比 debug 要清爽一些;
    • notice: 适用于生产模式;
    • warning : 警告信息;

6.6.4 logfile

如图

image-20230728090613879

  • logfile “” 就是说, 默认为控制台打印, 并没有日志文件生成
  • 可以为 redis.conf 的 logfile 指定配置项

6.6.5 databases 16

如图

image-20230728090759061

  • 设定库的数量 默认 16, 默认数据库为 0 号
  • 可以使用 SELECT 命令在连接上指定数据库 id

6.7 #SECURITY 安全#

6.7.1 设置密码

6.7.1.1 redis.conf 中设置密码

image-20230728090958792

6.7.1.2 命令行设置密码(密码只在本次启动过程中生效)

在redis服务中, config set requirepass 密码

在命令中设置密码, 是临时的, 重启 redis 服务器, 密码就还原了

6.8 #LIMITS 限制#

6.8.1 maxclients

image-20230728091350586

  • 设置 redis 同时可以与多少个客户端进行连接
  • 默认情况下为 10000 个客户端
  • 如果达到了此限制, redis会拒绝新的连接请求, 并且向这些连接请求方发出”max numberof clients reached”

6.8.2 maxmemory

image-20230728091506781

  • 在默认情况下, 对 32 位 实例会限制在 3 GB, 因为 32 位的机器最大只支持 4GB 的内存, 而系统本身就需要一定的内存资源来支持运行, 所以 32 位机器限制最大 3 GB 的可用内存是非常合理的, 这样可以避免因为内存不足而导致 Redis 实例崩溃
  • 在默认情况下, 对于 64 位实例是没有限制
  • 当用户开启了 redis.conf 配置文件的 maxmemory 选项, 那么 Redis 将限制选项的值不能小于 1 MB

对 maxmemory 设置的建议

  1. Redis 的 maxmemory 设置取决于使用情况, 有些网站只需要 32MB, 有些可能需要 12GB。

  2. maxmemory 只能根据具体的生产环境来调试, 不要预设一个定值, 从小到大测试,基本标准是不干扰正常程序的运行。

  3. Redis 的最大使用内存跟搭配方式有关, 如果只是用 Redis 做纯缓存, 64-128M 对一般小型网站就足够了

  4. 如果使用 Redis 做数据库的话, 设置到物理内存的 1/2 到 3/4 左右都可以

  5. 如果使用了快照功能的话, 最好用到 50%以下, 因为快照复制更新需要双倍内存空间,如果没有使用快照而设置 redis 缓存数据库, 可以用到内存的 80%左右, 只要能保证 Java、NGINX 等其它程序可以正常运行就行了

6.8.3 maxmemory-policy

image-20230728092049334

policy 一览:

  • volatile-lru: 使用 LRU 算法移除 key, 只对设置了过期时间的键; (最近最少使用)
  • allkeys-lru: 在所有集合 key 中, 使用 LRU 算法移除 key
  • volatile-random: 在过期集合中移除随机的 key, 只对设置了过期时间的键
  • allkeys-random: 在所有集合 key 中, 移除随机的 key
  • volatile-ttl: 移除那些 TTL 值最小的 key, 即那些最近要过期的 key
  • noeviction: 不进行移除。 针对写操作, 只是返回错误信息

6.8.4 maxmemory-samples

image-20230728091924891

  • 设置样本数量, LRU 算法和最小 TTL 算法都并非是精确的算法, 而是估算值, 所以你可以设置样本的大小, redis 默认会检查这么多个 key 并选择其中 LRU 的那个
  • 一般设置 3 到 7 的数字, 数值越小样本越不准确, 但性能消耗越小

7 发布和订阅

7.1 发布和订阅是什么

7.1.1 概述:

Redis 发布订阅 (pub/sub) 是一种消息通信模式: 发送者 (pub) 发送消息,订阅者 (sub) 接收消息

7.1.2 Redis 客户端可以订阅任意数量的频道

7.1.3 一图胜千言

1、 客户端订阅频道示意图

image-20230728092654620

2、当给这个频道发布消息后,消息就会发送给订阅的客户端

image-20230728093005557

7.2 如何理解发布和订阅模式

7.2.1 任务队列

1、 顾名思义, 就是”传递消息的队列”

2、 与任务队列进行交互的实体有两类, 一类是生产者( producer), 另一类则是消费者(consumer)。 生产者将需要处理的任务放入任务队列中, 而消费者则不断地从任务队列中读入任务信息并执行

7.2.2 如何理解

​ 可以这么简单的理解:

  1. Subscriber: 收音机, 可以收到多个频道, 并以队列方式显示
  2. Publisher: 电台, 可以往不同的 FM 频道中发消息
  3. Channel: 不同频率的 FM 频道

从 Pub/Sub 的机制来看, 它更像是一个广播系统, 多个订阅者(Subscriber) 可以订阅多个频道(Channel), 多个发布者(Publisher) 可以往多个频道(Channel) 中发布消息

7.3 发布订阅模式分类

——-发布:提供数据的

——-订阅:接受、处理数据的

7.3.1 一个发布者, 多个订阅者

​ -主要应用: 通知、 公告

​ -可以作为消息队列或者消息管道

​ -示意图

image-20230728093416754

7.3.2 多个发布者, 一个订阅者

​ -各应用程序作为 Publisher 向 Channel 中发送消息, Subscriber 端收到消息后执行相应的业务逻辑, 比如写数据库, 显示..

​ -主要应用:排行榜、投票、计数

​ -示意图

image-20230728093709916

7.3.3 多个发布者, 多个订阅者

-可以向不同的 Channel 中发送消息, 由不同的 Subscriber 接收。

-主要应用:群聊、聊天

-示意图

image-20230728093939273

7.4 命令行实现发布和订阅

7.4.1 发布、订阅操作

1、 PUBLISH channel msg

将信息 message 发送到指定的频道 channel

2、 SUBSCRIBE channel [channel …]

订阅频道, 可以同时订阅多个频道

3、 UNSUBSCRIBE [channel …]

取消订阅指定的频道, 如果不指定频道, 则会取消订阅所有频道

4、 PSUBSCRIBE pattern [pattern …]

订阅一个或多个符合给定模式的频道, 每个模式以 * 作为匹配符, 比如 it* 匹配所 有以 it 开头的频道( it.news 、 it.blog 、 it.tweets 等等), news.* 匹配所有 以 news. 开头的频道( news.it 、 news.global.today 等等), 诸如此类

5、 PUNSUBSCRIBE [pattern [pattern …]]

退订指定的规则, 如果没有参数则会退订所有规则

7.4.2 快速入门

1、 打开一个客户端订阅 channel1

image-20230728094614049

2、打开另一个客户端,给 channel1 发布消息 hello

image-20230728094636960

3、返回的 1, 是订阅者数量

4、发布的消息没有持久化(关闭redis后就没了)

5、订阅的客户端, 只能收到订阅后发布的消息

image-20230728094704271

8 Jedis

8.1 API 文档

8.1.1 在线文档 : https://www.mklab.cn/onlineapi/jedis/

8.2 Jedis 介绍

-Java 程序操作 Redis 的工具

8.3 Jedis 操作 Redis 数据

(以redis命名作为方法名称,学习成本低,但是线程不安全,多线程环境下需要基于连接池来使用)

8.3.1 快速入门

step1: 引入依赖
1
2
3
4
5
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.3.1</version>
</dependency>
step2:建立连接
1
2
3
4
5
6
7
8
9
10
11
12
/*** 老师解读
* 1. 确保 ip:6379 是连通的, 需要打开防火墙的端口
* 2. 如果 redis 你设置了密码, 需要执行 jedis.auth("hspedu");进行权限验证
*/
@Test
public void con(){
Jedis jedis = new Jedis("172.20.10.4", 6379);
jedis.auth("root");
String res = jedis.ping();
System.out.println("连接成功,返回结果:" + res);
jedis.close();
}
step3:使用Redis

(Jedis中的API和redis中的命令一模一样!)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void key(){
Jedis jedis = new Jedis("172.20.10.4", 6379);
jedis.auth("root");
jedis.set("q1","123");
jedis.set("q2","234");
jedis.set("q3","345");

Set<String> keys = jedis.keys("*");
for (String key : keys) {
System.out.println("key-->" + key);
}
System.out.println(jedis.exists("q1"));
System.out.println(jedis.ttl("q2"));
System.out.println(jedis.get("q2"));
jedis.close();
}
step4:释放资源
1
2
3
4
5
6
@AfterEach
void tearDown(){
if (jedis != null){
jedis.close();
}
}

8.3.2 连接 Redis 注意事项

1、 确保 ip:6379 是连通的, 注意打开防火墙的 6379 端口

1
2
3
4
5
6
● 设置开放的端口号
firewall-cmd --add-port=6379/tcp --permanent
● 重启防火墙
firewall-cmd --reload
● 查看防火墙
firewall-cmd --list-all

2、如果 redis 你设置了密码, 需要执行 jedis.auth(“密码”);进行权限验证

9 Spring Boot2 整合 Redis

9.1 需求分析/图解

  1. 在 springboot 中 , 整合 redis
  2. 可以通过 RedisTemplate 完成对 redis 的操作, 包括设置数据/获取数据
  3. 比如添加和读取数据

9.2 具体整合实现

9.2.1 创建 Maven 项目

9.2.2 修改 pom.xml - 引入相关依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

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

<!-- spring2.X集成redis所需common-pool-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<!--不要带版本号,防止冲突, 使用版本仲裁即可-->
</dependency>

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

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.2.2</version>
</dependency>

</dependencies>

9.2.3 配置

9.2.3.1 application.properties

– 完成 redis 的基本配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#Redis服务器地址
spring.redis.host=172.20.10.4
#Redis服务器连接端口
spring.redis.port=6379
#Redis密码
spring.redis.password=root
#Redis数据库索引(默认为0
spring.redis.database=0
#连接超时时间(毫秒
spring.redis.timeout=1800000
#连接池最大连接数(使用负值表示没有限制
spring.redis.lettuce.pool.max-active=20
#最大阻塞等待时间(使用负值表示没有限制
spring.redis.lettuce.pool.max-wait=-1
#连接池中最大空闲连接
spring.redis.lettuce.pool.max-idle=5
#连接池中最小空闲连接
spring.redis.lettuce.pool.min-idle=0

9.2.3.2 redis 配置类

1、 是对要使用的 RedisTemplate bean 对象的配置, 可以理解成是一个常规配置.

2、 同学们想一想我们以前学习过一个 JdbcTemplate,设计理念类似

3、 如果不配置, springboot 会使用默认配置, 这个默认配置, 会出现一些问题, 比如:redisTemplate 的 key 序列化等, 问题所以通常我们需要配置

4、创 建 RedisConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {

@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template =
new RedisTemplate<>();
System.out.println("template=>" + template);//这里可以验证..
RedisSerializer<String> redisSerializer =
new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer =
new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.WRAPPER_ARRAY);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(factory);
//key序列化方式
template.setKeySerializer(redisSerializer);
//value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}

@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer =
new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.WRAPPER_ARRAY);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}

9.2.3.3 注意事项和细节

1、 如果没有提供 RedisConfig 配置类 , springboot 会使用默认配置, 也可以使用,但是会存在问题

2、🌟 Unrecognized token ‘beijing’: was expecting (‘true’, ‘false’ or ‘null’)看报错,是 jason 转换异常,实际上是因为 redisTemplate 在做数据存储的时候会把存储的内容序列化,所以,redisTemplate 读取的时候也会反序列化,而在 redis 客户端set 的时候并不会做序列化,因此 set 的进去的值在用 redisTemplate 读的时候就会报类型转换异常了

—-解决方案 : 最简单的就是用程序重新 set 一遍即可

9.2.3.4编写控制层,接收前端消息后进行redis存取操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//装配RedisTemplate
@Resource
private RedisTemplate redisTemplate;

//编写方法,演示如何操作list,hash,set,zset
@GetMapping("/t2")
public String t2() {
//list-存
redisTemplate.opsForList().leftPush("books", "笑傲江湖");
redisTemplate.opsForList().leftPush("books", "hello,java");

//list-取出
List books = redisTemplate.opsForList().range("books", 0, -1);
String booksList = "";
for (Object book : books) {
System.out.println("book-->" + book.toString());
booksList += book.toString() + " ";
}
//hash
//redisTemplate.opsForHash()
//set
//redisTemplate.opsForSet()
//zset
//redisTemplate.opsForZSet()
return booksList;
}

RedisTemplate工具类的使用:

API 返回值类型 说明
redisTemplate.opsForValue() ValueOperations 操作String类型数据
redisTemplate.opsForHash() HashOperations 操作Hash类型数据
redisTemplate.opsForList() ListOperations 操作List类型数据
redisTemplate.opsForSet() SetOperations 操作Set类型数据
redisTemplate.opsForZSet() ZSetOperations 操作SortedSet类型数据
redisTemplate 通用的命令

10 Redis 持久化-RDB(记录数据)

10.1 官方资料

10.1.1 在线文档 : https://redis.io/topics/persistence

10.2 持久化方案

10.2.1 RDB(Redis DataBase)

10.2.2 AOF(Append Of File)

10.3 RDB 是什么?

10.3.1 在指定的时间间隔内将内存中的数据集快照写入磁盘, 也就 Snapshot 快照, 恢复时将快照文件读到内存

10.4 RDB 持久化流程

10.4.1 RDB 及其执行流程

1、 一图胜千言

image-20230729092028338

2、对上图的解读

​ 具体流程如下:

1) redis 客户端执行 bgsave 命令或者自动触发 bgsave 命令;
2) 主进程判断当前是否已经存在正在执行的子进程,如果存在,那么主进程直接返回;
3) 如果不存在正在执行的子进程,那么就 **fork 一个新的子进程进行持久化数据**,fork 过程是阻塞的,fork 操作完成后主进程即可执行其他操作;
4) 子进程先将数据写入到临时的 rdb 文件中,**待快照数据写入完成后再原子替换**旧的 rdb文件
  1. 同时发送信号给主进程,通知主进程 rdb 持久化完成,主进程更新相关的统计信息

3、老师小结

  1. 整个过程中,主进程是不进行任何 IO 操作的(交给子进程),这就确保了极高的性能
  2. 如果需要进行大规模数据的恢复, 且对于数据恢复的完整性不是非常敏感,那 RDB 方式要比 AOF 方式更加的高效
  3. RDB 的缺点是最后一次持久化后的数据可能丢失(最后一次持久化期间进来的数据不会被记录到rdb文件中)
  • 如果你是正常关闭 Redis , 关闭时会再进行一次持久化, 不会造成数据丢失
  • 如果是 Redis 异常终止/宕机, 就可能造成数据丢失
  • 后面在讲解快照配置 , 还会举例说明

10.4.2 Fork&Copy-On-Write

1、 Fork 的作用是复制一个与当前进程一样的进程。 新进程的所有数据(变量、 环境变量、程序计数器等) 数值都和原进程一致, 但是是一个全新的进程, 并作为原进程的子进程

——对进程进行深拷贝,并将结果作为原进程的子进程

2、 在 Linux 程序中, fork()会产生一个和父进程完全相同的子进程, 但子进程在此后多会exec 系统调用, 出于效率考虑, Linux 中引入了”写时复制技术 即: copy-on-write” , 有兴趣的参考: https://blog.csdn.net/Code_beeps/article/details/92838520

3、 一般情况父进程和子进程会共用同一段物理内存, 只有进程空间的各段的内容要发生变化时, 才会将父进程的内容复制一份给子进程。

10.5 RDB 配置

10.5.1 dump.rdb 文件

10.5.1.1 介绍

​ 在 redis.conf 中配置文件名称, 默认为 dump.rdb

image-20230729093257075

10.5.1.2 如何配置

1、 默认为 Redis 启动时命令行所在的目录下

​ 1) 如图

image-20230729094325579

说明:dump.rdb文件存放着redis中的数据,你每次启动,都会到你设定的路径下的 dump.rdb 中寻找数据,如果此处设置的不是绝对路径而是相对路径,那么你在不同的目录下启动redis时,就会去不同的地方寻找备份的数据,即在不同目录下启动得到的备份数据源不同!

10.5.2 相关配置&参数&操作

10.5.2.1 默认快照配置

1、 配置如图

image-20230729095330539

2、注意理解这个时间段的概念

image-20230729095537814

3、如果我们没有开启 save 的注释, 那么在退出 Redis 时, 也会进行备份, 更新 dump.db

10.5.2.2 save VS bgsave

1、 save : save 时只管保存, 其它不管, 全部阻塞。 手动保存, 不建议

2、 bgsave: Redis 会在后台异步进行快照操作, 快照同时还可以响应客户端请求。

3、 可以通过 lastsave 命令获取最后一次成功执行快照的时间(unix 时间戳) , 可以使用工具转换

https://tool.lu/timestamp/

10.5.2.3 flushall

1、执行 flushall 命令, 也会产生 dump.rdb 文件, 数据为空

2、Redis Flushall 命令用于清空整个 Redis 服务器的数据(删除所有数据库的所有 key)

10.5.2.4 Save

1、 格式: save 秒钟 写操作次数, 如图

image-20230729100520528

2、RDB 是整个内存的压缩过的 Snapshot,RDB 的数据结构,可以配置复合的快照触发条件(即配合save)

3、禁用: 给 save 传入空字符串, 可以看文档

10.5.2.5 stop-writes-on-bgsave-error

1、 配置如图

image-20230729100830128

2、当 Redis 无法写入磁盘的话(比如磁盘满了), 直接关掉 Redis 的写操作。推荐 yes

10.5.2.6 rdbcompression

1、 配置如图

image-20230729100916375

2、对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis 会采用LZF 算法进行压缩。

3、如果你不想消耗 CPU 来进行压缩的话,可以设置为关闭此功能, 默认 yes

10.5.2.7 rdbchecksum

1、 配置如图

image-20230729101019969

2、在存储快照后, 还可以让 redis 使用 CRC64 算法来进行数据校验,保证文件是完整的

3、但是这样做会增加大约 10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能, 推荐 yes

10.5.2.8 动态停止 RDB

1、 动态停止 RDB: redis-cli config set save “”

2、说明: save 后给空值,表示禁用保存策略

10.5.3 实例演示

1、 需求: 如果 Redis 的 key 在 30 秒内, 有 5 个 key 变化, 就自动进行 RDB 备份

修改redis.conf中的 save 30 5

10.6 RDB 备份&恢复

1、 关于 RDB 备份&恢复, 老韩要说的

  • 老韩先说明:Redis 可以充当缓存, 对项目进行优化, 因此重要/敏感的数据建议在 Mysql要保存一份
  • 从设计层面来说, Redis 的内存数据, 都是可以重新获取的(可能来自程序, 也可能来自Mysql)
  • 因此我们这里说的备份&恢复主要是给大家说明一下 Redis 启动时, 初始化数据是从dump.rdb 来的, 这个机制
  1. config get dir 查询 rdb 文件的目录

  2. 将 dump.rdb 进行备份, 如果有必要可以写 shell 脚本来定时备份 [参考 Linux 课程 定时备份 Mysql 数据库, 视频地址 https://www.bilibili.com/video/BV1Sv411r7vd?p=105 ] , 这里老师简单处理

10.7 RDB 持久化小结

10.7.1 优势

1、 适合大规模的数据恢复

2、对数据完整性和一致性要求不高更适合使用

3、节省磁盘空间

4、恢复速度快

10.7.2 劣势

  1. 虽然 Redis 在 fork 时使用了写时拷贝技术(Copy-On-Write), 但是如果数据庞大时还是比较消耗性能。

  2. 在备份周期在一定间隔时间做一次备份, 所以如果 Redis 意外 down 掉的话(如果正常关闭 Redis, 仍然会进行 RDB 备份, 不会丢失数据), 就会丢失最后一次快照后的所有修改

11 Redis 持久化-AOF(记录操作)

11.1 官方资料

11.1.1 在线文档 : https://redis.io/topics/persistence

11.2 AOF 是什么?

1、 AOF(Append Only File)

2、 以日志的形式来记录每个写操作(增量保存), 将 Redis 执行过的所有写指令记录下来(比如 set/del 操作会记录, 读操作 get 不记录) [后面演示]

3、 只许追加文件但不可以改写文件

4、 redis 启动之初会读取该文件重新构建数据

5、 redis 重启的话就根据日志文件的内容**将写指令从前到后执行一次以完成数据的恢复工作**

11.3 AOF 持久化流程

1、 持久化流程示意图

image-20230729155117545

2、解读

  1. 客户端的请求写命令会被 append 追加到 AOF 缓冲区
  2. AOF 缓冲区根据 AOF 持久化策略[always,everysec,no]将操作 sync 同步到磁盘的 AOF 文件
  3. AOF 文件大小超过重写策略或手动重写时,会对 AOF 文件 rewrite 重写,压缩 AOF 文件容量
  4. Redis 服务重启时,会重新 load 加载 AOF 文件中的写操作达到数据恢复的目的

11.4 AOF 开启

1、 在 redis.conf 中配置文件名称, 默认为 appendonly.aof

image-20230729155850603

2、AOF 文件的保存路径,同 RDB 的路径一致。

3、AOF 和 RDB 同时开启,系统默认取 AOF 的数据(默认AOF优先级高,可手动修改)

4、当开启 AOF 后, Redis 从 AOF 文件取数据.

11.5 AOF 实例演示

…………

11.6 AOF 启动/修复/恢复

11.6.1 基本说明

​ AOF 的备份机制和性能虽然和 RDB 不同, 但是备份和恢复的操作同 RDB 一样, 都是拷贝备份文件, 需要恢复时再拷贝到 Redis 工作目录下, 启动系统即加载

11.6.2 正常恢复

1、 修改默认的 appendonly no, 改为 yes

2、将有数据的 aof 文件定时备份, 需要恢复时, 复制一份保存到对应目录(查看目录:config get dir)

3、恢复:重启 redis 然后重新加载

4、和前面 RDB 备份/恢复机制类似

11.6.3 异常恢复

1、 如遇到 AOF 文件损坏, 通过 [/usr/local/bin]# ./redis-check-aof –fix appendonly.aof 进行恢复

2、建议先: 备份被写坏的 AOF 文件

3、恢复:重启 redis,然后重新加载

11.7 同步频率设置

1、 配置位置

image-20230729161749574

2、老韩解读上图

​ 1)appendfsync always

​ 始终同步,每次 Redis 的写入都会立刻记入日志;性能较差但数据完整性比较好

​ 2)appendfsync everysec

​ 每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。

​ 3)appendfsync noredis

​ 不主动进行同步,把同步时机交给操作系统

https://baijiahao.baidu.com/s?id=1740774723808931509&wfr=spider&for=pc

11.8 Rewrite 压缩

1、 rewrite 重写介绍

1) AOF 文件越来越大,**需要定期对 AOF 文件进行重写达到压缩**
1) 旧的 AOF 文件含有<u>无效命令会被忽略,保留最新的数据命令</u> , 比如 set a a1 ; set a b1 ;set a c1; 保留最后一条指令就可以了
1) <u>多条写命令可以合并为一个 , 比如 set a c1 b b1 c c1</u>
1) AOF 重写降低了文件占用空间
1) 更小的 AOF 文件可以更快的被 redis 加载

2、重写触发配置

​ 1) 手动触发直接调用 bgrewriteaof 命令

image-20230729162232144

​ 2)自动触发

image-20230729162701336

  • auto-aof-rewrite-min-size: AOF 文件最小重写大小, 只有当 AOF 文件大小大于该值时候才能重写, 默认配置 64MB
  • auto-aof-rewrite-percentage: 当前 AOF 文件大小和最后一次重写后的大小之间的比率等于或者大于指定的增长百分比,如 100 代表当前 AOF 文件是上次重写的两倍时候才重写

系统载入时或者上次重写完毕时,Redis 会记录此时 AOF 大小,设为base_size, 如果 Redis 的 AOF 当前大小>= base_size +base_size*100% (默认)且当前大小>=64mb(默认)的情况下,Redis 会对 AOF 进行重写

11.9 AOF 持久化小结

11.9.1 优势

1、 备份机制更稳健, 丢失数据概率更低。

2、可读的日志文本,通过操作 AOF 稳健,可以处理误操作

image-20230729175239380

11.9.2 劣势

1、 比起 RDB 占用更多的磁盘空间

2、恢复备份速度要慢

3、每次读写都同步的话,有一定的性能压力

11.10 RDB 还是 AOF?

1、 官方文档地址: https://redis.io/topics/persistence

2、官方推荐两个都启用

3、如果只做缓存:如果你只希望你的数据在服务器运行的时候存在, 你也可以不使用任何持久化方式

12 Redis事务_ 锁机制 _秒杀

12.1 Redis 的事务是什么?

​ 1、 Redis 事务是一个单独的隔离操作: 事务中的所有命令都会序列化、 按顺序地执行

​ 2、事务在执行的过程中,不会被其他客户端发送来的命令请求所打断

​ 3、Redis 事务的主要作用就是串联多个命令防止别的命令插队

12.2 Redis 事务三特性

12.2.1 单独的隔离操作

​ 1、 事务中的所有命令都会序列化、 按顺序地执行

​ 2、 事务在执行的过程中, 不会被其他客户端发送来的命令请求所打断

12.2.2 没有隔离级别的概念

​ 队列中的命令(指令), 在没有提交前都不会实际被执行

12.2.3 不保证原子性

​ 事务执行过程中, 如果有指令执行失败, 其它的指令仍然会被执行, 没有回滚

12.3 事务相关指令 Multi、 Exec、 discard

12.3.1 一图胜千言

1、 Redis 事务指令示意图

image-20230729180534083

2、解读上图:

  1. 从输入 Multi 命令开始,输入的命令都会依次进入命令队列中,但不会执行(类似 Mysql的 start transaction 开启事务)
  2. 输入 Exec 后,Redis 会将之前的命令队列中的命令依次执行(类似 Mysql 的 commit 提交事务)
  3. 组队的过程中可以通过 discard 来放弃组队(类似 Mysql 的 rollback 回顾事务)
  4. ⚠️: Redis 事务和 Mysql 事务本质是完全不同的, 这里只是用 Mysql 的做类似说明, 是为了让小伙伴好理解

12.3.2 快速入门

……

image-20230729181130039

12.3.3 注意事项和细节

​ 1、 组队的过程中, 可以通过 discard 来放弃组队

2、如果在组队阶段报错, 会导致 exec 失败, 那么事务的所有指令都不会被执行(这时候是有原子性的)

​ 3、如果组队成功, 但是指令有不能正常执行的, 那么 exec 提交, 会出现有成功有失败情况,也就是事务得到部分执行, 这种情况下, Redis 事务不具备原子性.

12.4 事务冲突及解决方案

12.4.1 先看一个问题

1、 经典的抢票问题(总共十张票)

​ 1) 一个请求想购买 6

​ 2) 一个请求想购买 5

​ 3) 一个请求想购买 1

——如果没有控制, 会造成超卖现象 ; 如果 3 个指令, 都得到执行, 最后剩余的票数是 -2

12.4.2 悲观锁

1、 工作示意图

image-20230729182017365

2、解读上图

  1. 悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在获取数据的时候都会上锁
  2. 这样别人/其它请求想拿这个数据就会 block,直到拿到锁。
  3. 悲观锁是锁设计理念, 传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁.

12.4.3 乐观锁

1、 工作示意图

image-20230729182519566

2、解读上图:

1) 乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁
1) 但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,**可以使用版本号等机制。**
1) 乐观锁<u>**适用于多读的应用类型,这样可以提高吞吐量**</u>。Redis 就是利用这种 check-and-set机制实现事务的
1) 乐观锁是锁设计理念(**不用担心在修改版本的瞬间,别人正好读到版本——因为版本修改阶段是原子性的!**)

12.4.4 watch & unwatch

12.4.4.1 watch

1、 基本语法: watch key [key …]

2、在执行 multi 之前,先执行 watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断.

3、这里可以结合乐观锁机制进行理解

image-20230729183535766

12.4.4.2 unwatch

1、 基本语法unwatch

2、取消 watch 命令对所有 key 的监视。

3、如果在执行 watch 命令后,exec 命令或 discard 命令先被执行了的话,那么就不需要再执行 unwatch 了

12.5 火车票-抢票

12.5.1 需求分析/图解

……

12.5.2 思路分析

​ – 思路分析

​ 1、一个 user 只能购买一张票, 即不能复购

​ 2、不能出现超购,也是就多卖了.

​ 3、不能出现火车票遗留问题/库存遗留, 即*火车票不能留下*

image-20230730102256386

12.5.3 版本 1: 完成基本购票流程, 暂不考虑事务和并发问题

​ 1、 创建 Java Web 项目, 参照以前讲过搭建 Java Web 项目流程即可

​ 2、引入相关的 jar 包 和 jquery

​ 3、创建 index.jsp(买票界面)

​ 4、创建 SecKillRedis.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/**
* 秒杀类: 完成秒杀,抢购
*/
public class SecKillRedis {

/**
* 编写一个测试方法-看看是否能够连通到指定的Redis
*/

@Test
public void testRedis() {

Jedis jedis = new Jedis("192.168.198.135", 6379);
//jedis.auth("foobared");//如果需要认证, 就使用auth
System.out.println(jedis.ping());
jedis.close();
}

/**
* 秒杀过程/方法
*/

/**
* @param uid 用户id - 在后台生成
* @param ticketNo 票的编号, 比如北京-成都的ticketNo 就是bj_cd
* @return
*/
public static boolean doSecKill(String uid, String ticketNo) {

//- uid 和 ticketNo进行非空校验
if (uid == null || ticketNo == null) {
return false;
}
//- 连接到Redis, 得到jedis对象
//Jedis jedis = new Jedis("192.168.198.135", 6379);

//- 通过连接池获取到jedis对象/连接
JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis = jedisPoolInstance.getResource();
System.out.println("---使用的连接池技术----");

//- 拼接票的库存key
String stockKey = "sk:" + ticketNo + ":ticket";

//- 拼接秒杀用户要存放到的set集合对应的key,这个set集合可以存放多个userId
String userKey = "sk:" + ticketNo + ":user";

//🎃监控库存
jedis.watch(stockKey);

//- 获取到对应的票的库存, 判断是否为null
String stock = jedis.get(stockKey);
if (stock == null) {
System.out.println("秒杀还没有开始, 请等待..");
jedis.close(); //如果jedis是从连接池获取的,则这里的close就是将jedis对象/连接释放到连接池
return false;
}

//- 判断用户是否重复秒杀/复购
if (jedis.sismember(userKey, uid)) {
System.out.println(uid + " 不能重复秒杀...");
jedis.close();
return false;
}

//- 判断火车票,是否还有剩余
if (Integer.parseInt(stock) <= 0) {
System.out.println("票已经卖完了, 秒杀结束..");
jedis.close();
return false;
}

////- 可以购买
////1. 将票的库存量-1
//jedis.decr(stockKey);
////2. 将该用户加入到抢购成功对应的set集合中
//jedis.sadd(userKey, uid);

//使用事务,完成秒杀(防止超卖)
Transaction multi = jedis.multi();

//组队操作
multi.decr(stockKey);//减去票的库存
multi.sadd(userKey, uid);//将该用户加入到抢购成功对应的set集合中

//执行
List<Object> results = multi.exec();

if(results == null || results.size() == 0) {
System.out.println("抢票失败...");
jedis.close();
return false;
}

System.out.println(uid + " 秒杀成功..");
jedis.close();
return true;

}
}

5、创建SecKillServlet.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class SecKillServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 请求时,模拟生成一个userId
String userId = new Random().nextInt(10000) + "";
//2. 获取用户要购买的票的编号
String ticketNo = request.getParameter("ticketNo");

//3. 调用秒杀的方法
//boolean isOk = SecKillRedis.doSecKill(userId, ticketNo);

//4. 调用lua脚本完成秒杀方法
boolean isOk = SecKillRedisByLua.doSecKill(userId, ticketNo);

//4. 将结果返回给前端-这个地方可以根据业务需要调整
response.getWriter().print(isOk);
}

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doPost(request, response);
}
}

12.5.4 抢票并发模拟, 出现超卖问题

并发模拟

可以使用postman模拟

image-20230730110008609

12.5.5 连接池技术

12.5.5.1 连接池介绍

​ 1、 节省每次连接 redis 服务带来的消耗, 把连接好的实例反复利用。

​ 2、 链接池参数

    - MaxTotal: 控制**一个 pool 可分配多少个 jedis 实例**, 通过 pool.getResource()来获取;如果赋值为-1, 则表示不限制
    - maxIdle: 控制一个 pool 最多有**多少个状态为 idle(空闲)的 jedis 实例**
  • MaxWaitMillis: 表示当获取一个 jedis 实例时, 最大的等待毫秒数, 如果超过等待时间,则直接抛 JedisConnectionException

  • testOnBorrow: 获得一个 jedis 实例的时候是否检查连接可用性(ping()); 如果为 true,则得到的 jedis 实例均是可用的

12.5.5.2 使用连接池, 优化连接超时

​ 1、 老师说明: 通过连接池, 可以指定连接超时时间, 这个连接超时时间, 也需要合理设置 , 要考虑到用户的实际体验

​ 2 、 创 建 JedisPoolUtil.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* 使用连接池的方式来获取Redis连接
*/
public class JedisPoolUtil {

//老师解读volatile作用
//1. 线程的可见性: 当一个线程去修改一个共享变量时, 另外一个线程可以读取这个修改的值
//2. 顺序的一致性: 禁止指令重排
private static volatile JedisPool jedisPool = null;


private JedisPoolUtil() {

}

//保证每次调用返回的 jedisPool是单例-这里老师使用了双重校验
public static JedisPool getJedisPoolInstance() {

if (null == jedisPool) {
synchronized (JedisPoolUtil.class) {
if (null == jedisPool) {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
//对连接池进行配置
jedisPoolConfig.setMaxTotal(200);
jedisPoolConfig.setMaxIdle(32);
jedisPoolConfig.setMaxWaitMillis(60 * 1000);
jedisPoolConfig.setBlockWhenExhausted(true);
jedisPoolConfig.setTestOnBorrow(true);
jedisPool = new JedisPool(jedisPoolConfig, "192.168.198.135", 6379, 60000);
}
}
}
return jedisPool;
}

//释放连接资源
public static void release(Jedis jedis) {

if(null != jedis) {
jedis.close();//如果这个jedis是从连接池获取的,这里jedis.close(),就是将jedis对象/连接,释放到连接池
}

}


}

🌟volatile:

这是因为 new 关键字创建对象不是原⼦操作,创建⼀个对象会经历下⾯
的步骤:

  1. 在堆内存开辟内存空间
  2. 调⽤构造⽅法,初始化对象
  3. 引⽤变量指向堆内存空间

​ 为了提⾼性能,编译器和处理器常常会对既定的代码执⾏顺序进⾏指令重排序,从源码到最终执⾏指令会经历如下流程:
源码编译器优化重排序指令级并⾏重排序内存系统重排序最终执⾏指令序列所以经过指令重排序之后,创建对象的执⾏顺序可能为 1 2 3 或者 1 3 2 ,因此当某个线程在乱序运⾏ 1 3 2 指令的时候,引⽤变量指向堆内存空间,这个对象不为 null,但是没有初始化,其他线程有可能这个时候进⼊了 getInstance 的第⼀个 if(instance == null) 判断不为 nulll ,导致错误使⽤了没有初始化的⾮ null 实例,这样的话就会出现异常,这个就是著名的
DCL 失效问题。
当我们在引⽤变量上⾯添加 volatile 关键字以后,会通过在创建对象指令的前后添加内存屏障来禁⽌指令重排序,就可以避免这个问题,⽽且对volatile 修饰的变量的修改对其他任何线程都是可⻅的。

12.5.6 利用 Reids 事务机制, 解决超卖

1、 控制超卖-Redis 事务底层(乐观锁机制分析)

image-20230730133348196

2 、 修 改 SecKillRedis.java

😵‍💫见12.5.3 的完整代码

12.5.7 抢票并发模拟,出现库存遗留问题

1、 先重置一下 redis 的数据,把库存量设的较大 , 为 600

2、执行指令

​ ab -n 1000 -c 300 -p ~/postfile -T application/x-www-form-urlencodedhttp://ip:port/seckill/secKillServlet

​ 老韩解读:1) 这里我们并发数变大 -c 300

3、执行结果

image-20230730134144402

​ 可以看到, 剩余票数为 543, 并不是 0

4、出现库存遗留问题的分析

image-20230730134242569

12.5.8 LUA 脚本

12.5.8.1 LUA 介绍

​ 1、 Lua 是一个小巧的脚本语言, Lua 脚本可以很容易的被 C/C++ 代码调用, 也可以反过来调用 C/C++的函数, Lua 并没有提供强大的库, 一个完整的 Lua 解释器不过 200k,所以 Lua 不适合作为开发独立应用程序的语言, 而是作为嵌入式脚本语言

​ 2、很多应用程序、游戏使用 LUA 作为自己的嵌入式脚本语言,以此来实现可配置性、可扩展性。

​ 3、将复杂的或者多步的 Redis 操作,写为一个脚本,一次提交给 redis 执行,减少反复连接 redis 的次数。提升性能。

​ 4、LUA 脚本是类似 Redis 事务,有一定的原子性,不会被其他命令插队,可以完成一些 redis 事务性的操作

​ 5、Redis 的 lua 脚本功能,只有在 Redis 2.6 以上的版本才可以使用

​ 6、通过 lua 脚本解决争抢问题,实际上是 Redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题

12.5.8.2 LUA 脚本, 解决库存遗留-思路分析图

​ 一图胜千言

image-20230730135547720

对上图解读

1) LUA 脚本是类似 Redis 事务,有一定的原子性,不会被其他命令插队,能完成 Redis事务性的操作 
1) 通过 lua 脚本解决争抢问题,Redis 利用其单线程的特性,将请求形成任务队列, 从而解决多任务并发问题

12.5.8.3 LUA 脚本, 解决库存遗留-代码实现

1、 编写 lua 脚本文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/**
* 使用Lua脚本完成秒杀
*/
public class SecKillRedisByLua {

/**
* 老师说明
* 1. 这个脚本字符串是在lua脚本上修改的, 但是要注意不完全是字符串处理
* 2. 比如 : 这里我就使用了 \" , 还有换行使用了 \r\n
* 3. 这些都是细节,如果你直接把lua脚本粘贴过来,不好使,一定要注意细节
* 4. 如果写的不成功,就在老师这个代码上修改即可
*/
static String secKillScript = "local userid=KEYS[1];\r\n" +
"local ticketno=KEYS[2];\r\n" +
"local stockKey='sk:'..ticketno..\":ticket\";\r\n" +
"local usersKey='sk:'..ticketno..\":user\";\r\n" +
"local userExists=redis.call(\"sismember\",usersKey,userid);\r\n" +
"if tonumber(userExists)==1 then \r\n" +
" return 2;\r\n" +
"end\r\n" +
"local num= redis.call(\"get\" ,stockKey);\r\n" +
"if tonumber(num)<=0 then \r\n" +
" return 0;\r\n" +
"else \r\n" +
" redis.call(\"decr\",stockKey);\r\n" +
" redis.call(\"sadd\",usersKey,userid);\r\n" +
"end\r\n" +
"return 1";

//使用lua脚本完成秒杀的核心方法
public static boolean doSecKill(String uid,String ticketNo) {
//先从redis连接池,获取连接
JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis = jedisPoolInstance.getResource();
//就是将lua脚本进行加载
String sha1 = jedis.scriptLoad(secKillScript);
//evalsha是根据指定的 sha1校验码, 执行缓存在服务器的脚本
Object result = jedis.evalsha(sha1, 2, uid, ticketNo);
String resString = String.valueOf(result);

//根据lua脚本执行返回的结果,做相应的处理
if("0".equals(resString)) {
System.out.println("票已经卖光了..");
jedis.close();
return false;
}

if("2".equals(resString)) {
System.out.println("不能重复购买..");
jedis.close();
return false;
}

if("1".equals(resString)) {
System.out.println("抢购成功");
jedis.close();
return true;
} else {
System.out.println("购票失败..");
jedis.close();
return false;
}
}
}

13 主从复制

13.1 主从复制介绍

13.1.1 分析单个 Redis 的问题

13.1.2 一图胜千言

1、 Redis 主从复制的示意图

image-20230730230820880

2、对上图的解读

  1. 上图描述了主机数据更新后, 自动同步到备机的 master/slaver 机制

  2. Master 以为主,Slaver 以为主

  3. 好处: 读写分离, 提升效率 (理解: 读写分离后, 将读和写操作分布到不同的 Reids, 减少单个 Redis 的压力, 提升效率)

  4. 好处: 容灾快速恢复 (理解: 如果某个 slaver , 不能正常工作, 可以切换到另一个 slaver)

  5. 主从复制, 要求是 1 主多从, 不能有多个 Master( 理解: 如果有多个主服务器 Master,那么 slaver 不能确定和哪个 Master 进行同步, 出现数据紊乱)

  6. 要解决主服务器的高可用性, 可以使用 Redis 集群

13.2 搭建一主多从

1、 需求说明

  1. 搭建主从复制结构
  2. 这里我们搭建 一主二从即可, 其它 slaver 可以依此完成
  3. 分析我搭建的思路/认真理解

把原来的redis.conf复制三份,分别进行配置,用它们来各自启动redis服务,再通过指令:「slaveof 」来设置主从关系,这样以后我们就搭建完毕了

注意:

  • 搭建完以后,在主服务中添加、修改数据,可以在从服务处读取
  • 从服务无法添加、修改、删除数据,只能读取

13.3 主从复制-原理

1、 原理示意图

image-20230730233615428

2、老师解读上图-主从复制流程

  • Slave 启动成功连接到 master 后会发送一个 sync 命令
  • Master 接到命令启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令, 在后台进程执行完毕之后, master 将传送整个数据文件到 slave,以完成一次完全同步
  • slave 服务在接收到数据库文件数据后,将其存盘并加载到内存中, 即 全量复制
  • Master 数据变化了, 会将新的收集到的修改命令依次传给 slave, 完成同步, 即 增量复制
  • 但是只要是重新连接 master,一次完全同步(全量复制)将被自动执行

总结:

(第一次连 –> 全量复制;连上后主服务数据变化 –> 增量复制;断开后重新连上 –> 全量复制)

13.4 一主二仆

1、 如果从服务器 down 了, 重新启动, 仍然可以获取 Master 的最新数据

2、如果主服务器 down 了, 从服务器并不会抢占为主服务器, 当主服务器恢复后, 从服务器仍然指向原来的主服务器.

13.5 薪火相传

1、 示意图

image-20230731082350913

2、老师解读上图

1) 上一个 Slave 可以是下一个 slave 的 Master,Slave 同样可以接收其他 slaves 的连接和同步请求,那么该 <u>slave 作为了链条中下一个的 master, 可以有效减轻 master 的写压力,</u>去中心化降低风险
1) 用 **slaveof <master_ip><master_port>     (从服务的ip、port)**
  1. 风险是一旦某个 slave 宕机,后面的 slave 都没法同步
  2. 主机挂了,从机还是从机,无法写数据了
  3. 作为别人master的从服务只是拥有了与其从服务同步数据的能力,并不能修改数据!

13.6 反客为主

1、 在薪火相传的结构下, 当一个 master 宕机后, 指向 Master 的 slave 可以升为 master, 其后面的 slave 不用做任何修改

2、用 slaveof no one 将从机变为主机 (老韩说明: 后面可以使用哨兵模式, 自动完成切换.)

13.7 哨兵模式(sentinel)

13.7.1 实例演示

1、 工作示意图

image-20230731083436662

2、哨兵模式(如图): **反客为主的自动版**,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库

3、老韩实验

​ 1)调整为一主二仆模式,6379 带着 6380、6381 , 根据前面讲解的调整即可

​ 2)创建 sentinel.conf , 名字不能乱写, 按照指定的来

写入:sentinel monitor redis_master 127.0.0.1 6379 1

​ 说明:

redis_master 为监控对象起的服务器名称

1 表示至少有多少个哨兵同意迁移的数量, 这里我配置1 表示只要有1个哨兵同意迁移就可以切换

​ 3) 启动哨兵, 注意看哨兵的端口是 26379

image-20230731084427183

​ 4) 当主机挂掉,从机选举中产生新的主机

​ 5) 如果原来的主机重启, 会自动成为从机

13.7.2 注意事项和细节

1、 在哨兵模式下, 主机 down 后的执行流程分析

image-20230731084948786

2、解读上图 - 哨兵如何在从机中, 推选新的 Master 主机, 选择的条件依次为:

  1. 优先级在 redis.conf 中默认:replica-priority 100,值越小优先级越高
  2. 偏移量是指获得原主机数据的量, 数据量最全的优先级高
  3. 每个 redis 实例启动后都会随机生成一个 40 位的 runid, 值越小优先级越高

14 集群

14.1 为什么需要集群-高可用性

1、 生产环境的实际需求和问题

  • 容量不够,redis 如何进行扩容?
  • 并发写操作, redis 如何分摊?
  • 主从模式,薪火相传模式,主机宕机,会导致 ip 地址发生变化,应用程序中配置需要修改对应的主机地址、端口等信息

2、传统解决方案-代理主机来解决

image-20230731090153302

解读上图

  1. 客户端请求先到代理服务器

  2. 由代理服务器进行请求转发到对应的业务处理服务器

  3. 为了高可用性, 代理服务、A 服务、B 服务、C 服务都需要搭建主从结构(至少是一主一从),这样就需求搭建至少 8 台服务器

  4. 这种方案的缺点是: 成本高,维护困难, 如果是一主多从, 成本就会更高

3、redis3.0 提供解决方案-无中心化集群配置

image-20230731090351617

解读上图

1) 各个 Redis 服务仍然采用主从结构
2) 各个 Redis 服务是连通的, **任何一台服务器, 都可以作为请求入口**
3) 各个 Redis 服务器因为是连通的, 可以进行请求转发
4) 这种方式, 就无中心化集群配置, 可以看到,只需要 6 台服务器即可搞定
  1. 无中心化集群配置, 还会根据 key 值, 计算 slot , 把数据分散到不同的主机, 从而缓解单个主机的存取压力.[后面老师会演示和再说明]
  2. Redis 推荐使用无中心化集群配置
  3. 在实际生成环境 各个 Redis 服务器, 应当部署到不同的机器(防止机器宕机, 主从复制失效)

14.2 集群介绍

1、 Redis 集群实现了对 Redis 的水平扩容, 即启动 N 个 redis 节点, 将整个数据库分布存储在这 N 个节点中, 每个节点存储总数据的 1/N。

2、 Redis 集群通过分区(partition) 来提供一定程度的可用性(availability): 即使集群中有一部分节点失效或者无法进行通讯, 集群也可以继续处理命令请求

实现:

1、修改主服务的conf文件中的配置:

​ cluster-enabled yes 打开集群模式

​ cluster-config-file nodes-6379.conf

​ 设定节点配置文件名cluster-node-timeout 15000 设定节点失联时间,超过该时间(毫秒),集群自动进行主从切换

2、启动服务

image-20230731093504555

image-20230731093522619

3、将六个节点合成一个集群

image-20230731093608968

将六个节点合成一个集群 的指令:

1
redis-cli --cluster create --cluster-replicas 1 192.168.198.130:6379 192.168.198.130:6380 192.168.198.130:6381 192.168.198.130:6389192.168.198.130:6390 192.168.198.130:6391
注意事项和细节
  1. 组合之前,请确保所有 redis 实例启动后,nodes-xxxx.conf 文件都生成正常
  2. 此处不要用 127.0.0.1, 请用真实 IP 地址, 在真实生产环境, IP 都是独立的.
  3. replicas 1 采用最简单的方式配置集群,一台主机,一台从机,正好三组
  4. 搭建集群如果没有成功, 把 sentinel 进程 kill 掉, 再试一下
  5. 分析主从对应关系

4、集群方式登录

指令: redis-cli -c -p 6379

指令: cluster nodes 命令查看集群信息, 主从的对应关系

14.3.2 注意事项和细节

1、 一个集群至少要有三个主节点

2、 选项 –cluster-replicas 1 表示我们希望为集群中的每个主节点创建一个从节点

3、 分配原则: 尽量保证主服务器和从服务器各自运行在不同的 IP 地址(机器), 防止机器故障导致主从机制失效, 高可用性得不到保障

14.4 Redis 集群使用

1、 什么是 slots

1)Redis 集群启动后, 你会看到如下提示

image-20230731094714287

image-20230731094732477

2)一个 Redis 集群包含 16384 个插槽(hash slot),编号从 0-16383, Reids 中的每个键都属于这 16384 个插槽的其中一个

3)集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽, 其中 CRC16(key) 语句用于计算键 key 的 CRC16 校验和

image-20230731094808015

4)集群中的每个节点负责处理一部分插槽。 举个例子, 如果一个集群可以有主节点, 其中:

  • 节点 A 负责处理 0 号至 5460 号插槽
  • 节点 B 负责处理 5461 号至 10922 号插槽
  • 节点 C 负责处理 10923 号至 16383 号插槽

2、在集群中录入值

1)在 redis 每次录入、查询键值,redis 都会计算出该 key 应该送往的插槽,如果不是该客户端对应服务器的插槽,redis 会告知应前往的 redis 实例地址和端口。 (因为你可以在集群的任意端启用redis服务)

2)redis-cli 客户端提供了 –c 参数实现自动重定向

3)如 redis-cli -c –p 6379 登入后,再录入、查询键值对可以自动重定向

image-20230731095430316

4)**不在一个 slot 下的键值,是不能使用 mget,mset 等多键操作**

image-20230731095522774

5)可以通过{}来定义组的概念,从而使 key 中{}内相同内容的键值对放到一个 slot 中,可以对一个组的数据进行mget等多键操作

image-20230731095552328

3、查询集群中的值

1)指令: CLUSTER KEYSLOT 返回 key 对应的 slot 值

2)指令: CLUSTER COUNTKEYSINSLOT 返回 slot 有多少个 key

3)指令: CLUSTER GETKEYSINSLOT 返回 count 个 slot 槽中的键

image-20230731100125306

14.5 Redis 集群故障恢复

1、 如果主节点下线, 从节点会自动升为主节点(注意 15 秒超时, 再观察比较准确)

—是该主节点的从节点提生成master,而不是其他节点替代这个节点

2、主节点恢复后,主节点回来变成从机

3、如果所有某一段插槽的主从节点都宕掉,Redis 服务是否还能继续, 要根据不同的配置而言

​ 1)如果某一段插槽的主从都挂掉,而 cluster-require-full-coverage 为 yes ,那么 ,整个集群都挂掉

​ 2)如果某一段插槽的主从都挂掉,而 cluster-require-full-coverage 为 no , 那么, 只是该插槽数据不能使用,也无法存储

​ 3)redis.conf 中的参数 cluster-require-full-coverage

image-20230731100749215

14.6 集群的 Jedis 开发

1、 即使连接的不是主机, 集群会自动切换主机存储。 主机写, 从机读。

2、无中心化主从集群。无论从哪台主机写的数据,其他主机上都能读到数据

3、老韩实验

​ 1)将 Redis 相关的端口都打开(防火墙设置), 否则会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 操作Redis集群
*/
public class JedisCluster_ {
public static void main(String[] args) {

/**
* 1. 这里的set可以加入多个入口
* 2. 因为我们没有做日志配置,输出时,有些提示,但是不影响使用
* 3. 如果我们使用的是集群,需要把相关的端口都打开,否则会报错
* 4. JedisCluster的构造器有多个,也可以直接传入HostAndPort 代码如下
* public JedisCluster(HostAndPort node) {
* this(Collections.singleton(node));
* }
*
*/
Set<HostAndPort> set = new HashSet<HostAndPort>();
set.add(new HostAndPort("192.168.198.135",6379));

JedisCluster jedisCluster = new JedisCluster(set);
jedisCluster.set("address","bj~北京");
String address = jedisCluster.get("address");
System.out.println("address-->" + address);
jedisCluster.close();

}
}

14.7 Redis 集群的优缺点

14.7.1 优点

​ 1、 实现扩容

​ 2、分摊压力

​ 3、无中心配置相对简单

14.7.2 缺点

​ 1、 多键操作是不被支持的

​ 2、 多键的 Redis 事务是不被支持的。 lua 脚本不被支持

​ 3、 由于集群方案出现较晚, 很多公司已经采用了其他的集群方案, 而其它方案想要迁移至 redis cluster, 需要整体迁移而不是逐步过渡, 复杂度较大

15 缓存:

15.1缓存穿透

15.1.1缓存穿透的原因

​ 指客户端请求的数据在redis缓存中和数据库中都不存在,在redis中找不到数据,这些请求都会打到数据库,可能压垮数据库。

15.1.2解决方案:

缓存空对象:

​ 我们把查不到的数据当作null存到redis中,防止再去数据库中找没有的值;我们需要给空结果的过期时间设得短一些,防止中途数据库中产生了刚刚查不到的数据,但是请求一直被卡在redis的情况

​ 优点:实现简单,维护方便

​ 缺点:额外的内存消耗、 可能造成短期的不一致

image-20230416215655097

设置可访问的白名单

​ 只让一些选定的人访问,防止被刷

布隆过滤器

​ 优点:内存占用较少,没有多余key

​ 缺点:实现复杂、 存在误判可能(hash冲突)

进行实时监控

​ 当发现redis的命中率开始急速降低时,需要排查访问对象和访问的数据,需要专门派人监督,酌情使用黑名单或限制服务

15.2缓存击穿

15.2.1缓存击穿的原因

1)key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期,会从后端DB加载数据并回设到缓存,这时大并发的请求可能会瞬间把后端DB压垮

2)比如某个热点数据,可能会在某些时问点,被超高并发地访问,容易出现缓存击穿

15.2.2缓存击穿的现象/表象

1)数据库访问压力瞬时增加

2)Redis里面没有出现大量key过期

3)Redis正常运行状态,但是数据库可能瘫痪了

15.2.3解决方案

  • 预先设置热门数据

    • 在redis高峰访问之前,把一些数据提前存入Reids中,并加大这些数据的过期时间
  • 实时调整

    • 现场监控哪些数据热门,实时调整key的过期时长
  • 使用锁,如图:

    image-20230731145946233

15.3缓存雪崩

15.3.1缓存雪崩的原因

1)key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存

2)这个时候大并发的请求可能会瞬间把后端DB压垮。

3)缓存雪崩与缓存击穿的区别在于雪崩针对很多key缓存同时过期,前者则是某一个key

15.3.2缓存雪崩的现象/表象

1)数据库访问压力变大,服务器崩溃

2)在极短时间内,访问大量Key,而**这些Key集中过期**

15.3.3解决方案

1)构建多级缓存架构

nginx缓存+redis缓存 +其他缓存(ehcache等),这种方式开发/ 维护成本较高

2)使用锁或队列

用加锁或者队列的方式来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用高并发情况。

3)设置过期标志更新缓存

记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际key的缓存。

4)将缓存失效时间分散开

15.4分布式锁

1、单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效

3、单纯的Java API并不能提供分布式锁的能力

4、为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解決的问题

5、示意图(说明:我们探讨的分布式锁是针对分布式项目/架构而言)

image-20230731151840509

image-20230731152158961

15.4.1分布式锁主流实现方案:

  • 基于数据库实现
  • 基于缓存(Redis等)——性能最高
  • 基于Zookeeper———-可靠性最高

Redis实现分布式锁

命令:setnx key value

​ 首先,当然是搭建一个最简单的实现方式,直接用Redis的setnx命令,这个命令的语法是:setnx key value如果key不存在,则会将key设置为value, 并返回1;如果key存在,不会有任务影响,返回0。

image-20230731155434255
⚠️:为了防止获取锁的服务挂掉,导致锁一直不被释放,我们需要为锁添加超时时间

命令: set key value nx ex seconds 其中nx表示保证操作的原子性 ex表示增加了过期时间,seconds为秒数

image-20230731160005527
⚠️:同样的一把锁,服务A可能挂掉服务B的锁

具体场景:A持有锁,A执行其业务,但由于业务流程长或者网络延迟导致锁过期了,这时候B拿到了锁,在B运行业务时,A做完业务回来了!并释放了锁!这就导致了上述情景。

解决方案:为🔒加上owner

命令: set key value nx ex seconds ——-这里都value就是owner!!!!

⚠️:如果你获取锁的时候还是锁的owner,但是因为操作不具有原子性,正当你准备删除时,锁过期了并且被其他服务获取,这时执行删除会造成误删!

业务执行完成之后,检查锁、删除锁这些操作不是原子化的,这会导致上述情况发生

我们要想办法把这些操作整合成原子操作——Lua

image-20230731161136468
⚠️:即便这样,可靠性还是没有得到保证

​ 我们可以为redis配置从节点、哨兵模式等方式一定程度上提高可靠性

没有完全可靠的分布式锁,需要与业务联动配合,进行取舍,选择最合适的方案。