学静思语
Published on 2025-02-15 / 7 Visits
0
0

Redis

Redis

一、Redis基础知识

1. 相关资料

  • 官网:https://redis.io/
  • 中文地址:https://www.redis.net.cn/
  • 下载地址:https://redis.io/downloads/

2. 为什么需要Redis

2.1 企业需求

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

2.2 关系型数据库

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

2.3 Redis的优势

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

3. Redis简介

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

3.1 特征

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

3.2 应用场景

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

二、NoSQL数据库

  • 一句话: 即 Not-OnlySQL( 泛指非关系型的数据库),作为关系型数据库的补充
  • 作用:应对在海量用户和海量数据的情况下,带来的数据处理问题

1. NoSQL 的特点

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

2. 常见 Nosql 数据库

  • Redis

  • memcache

  • HBase

  • MongoDB

三、Redis安装和下载

1. Redis下载

  • 下载地址:https://redis.io/downloads/
  • 在实际开发中Redis都在Linux下工作,Redis版本:Redis6

2. Redis安装

  • 重新配置网络配置

  • DNS 参考:https://zhidao.baidu.com/question/1869986988897128467.html

  • 安装C语言的编译环境

  • 指令:yum install gcc

  • 查看版本:gcc –version

  • 下载redis-6.2.6.tar.gz 上传到 /opt目录

    image-20241207152516779

  • 进入到 /opt 目录, 执行解压命令:tar-zxvf redis-6.2.6.tar.gz

image-20241207152815049

  • 解压完成后, 进入目录:cd redis-6.2.6

    image-20241207153112977

  • 在redis-6.2.6 目录下, 执行make命令(编译指令)

    image-20241207153128914

  • 提示:

    • 如果没有准备好 C 语言编译环境,make 会报错— Jemalloc/jemalloc.h:没有那个文件
    • 解决方案:运行make distclean , 在执行 make 指令即可.
  • 执行: make install, 进行安装

    image-20241207153335076

  • 到此,安装OK , 安装目录在 /usr/local/bin

  • 查看默认安装目录 image-20241207153426165

  • redis-benchmark:性能测试工具,可以在自己机器运行,看看自己机器性能如何

  • redis-check-aof:修复有问题的AOF文件,rdb和aof后面会记录

  • redis-check-dump:修复有问题的dump.rdb 文件

  • redis-sentinel:Redis 集群使用

  • redis-server:Redis 服务器启动命令

  • redis-cli:客户端,操作入口

3. Redis后台启动和使用

  • 拷贝一份redis.conf 到其他目录, 比如 /etc 目录, 注意执行保证能够定位到 redis.conf,该文件在解压的目录中

  • 指令:cp redis.conf /etc/redis.conf

    image-20241207154831820

    image-20241207154938697

  • 修改/etc/redis.con 后台启动设置daemonizeno 改成yes, 并保存退出.

    image-20241207155201175

  • Redis 启动, 注意保证能定位redis-server 指令

  • 指令:/usr/local/bin/redis-server /etc/redis.conf

    image-20241207155604821

  • 查看redis 是否后台启动成

  • 指令:ps -aux | grep redis

image-20241207155643299

  • 用客户端访问
  • 指令:redis-cli

image-20241207155721246

  • 指定端口方式
  • 指令:redis-cli -p 6379

image-20241207155824024

  • Redis 关闭

  • 单实例关闭

    • 指令:redis-cli shutdownimage-20241207160151680
  • 多实例关闭,指定端口关闭

    • 指令:redis-cli -p 6379 shutdown

      image-20241207160330877

  • 也可以进入redis 再关闭

    • 指令:shutdown

      image-20241207160455484

四、Redis指令

  • 指令文档:https://www.redis.net.cn/tutorial/3506.html

1. 基础操作

  • set key value : 设置 key,value 数据
  • get key : 根据 key 查询对应的 value,如果不存在,返回空(nil)
  • clear : 清除屏幕中的信
  • quit/exit : 退出客户端 【说明:Redis 服务没有结束】
  • help 命令名称: 获取命令帮助文档,获取组中所有命令信息名称

2. 对Key(键)操作

  • keys * : 查看当前库所有key (匹配:keys*1)
  • exists key:判断某个 key 是否存在
  • type key :查看你的key是什么类型
  • del key : 删除指定的key数据
  • unlink key:根据value 选择非阻塞删除【仅将keys从keyspace元数据中删除,真正的删除会在后续异步操作】
  • expire key 10 : 10 秒钟:为给定的key设置过期时间
  • ttl key 查看还有多少秒过期,-1表示永不过期,-2表示已过期

3. 对DB(数库操作)

  • select: 命令切换数据库

  • redis 安装后,默认有16个库,0-15

    image-20241208144207213

  • 默认操作的是 redis 的 0号库

  • dbsize:查看当前数据库的key的数量

  • flushdb:清空当前库

  • flushall:清空全部库

五、Redis五大数据类型/结构

1. 操作文档

  • 官方文档:https://redis.io/docs/latest/commands/
  • 中文文档:https://www.redis.net.cn/order/

2.Redis数据存储格式

  • 一句话:redis 自身是一个 Map,其中所有的数据都是采用 key:value 的形式存储
  • key 是字符串,value 是数据,数据支持多种类型/结构

3.Redis数据类型-5种常用

  • string
  • hash
  • list
  • set
  • sorted_set

4. string

4.1 说明

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

4.2 String常用指令和演示

  • set < key> < value>添加键值对
  • get < key>查询对应键值
  • append < key> < value>将给定的< value> 追加到原值的末尾
  • strlen < key>获得值的长度
  • setnx < key> < value>只有在 key 不存在时设置 key 的值
  • incr < key> 将 key 中储存的数字值(字符串)增1, 只能对数字值操作,如果为空,新增值为1
  • decr < key> 将 key 中储存的数字值(字符串)减1 , 只能对数字值操作,如果为空,新增值为 -1
  • incrby/decrby < key> < 步长>将 key 中储存的数字值增减。自定义步长
  • mset < key1>< value1> < key2>< value2> ….., 同时设置一个或多个 key-value 对
  • mget < key1> < key2> < key3> ….. 同时获取一个或多个 value
  • msetnx < key1>< value1> < key2>< value2> ….. 同时设置一个或多个 key-value 对,当且仅当所有给定key 都不存在, 原子性,有一个失败则都失败
  • getrange < key> < 起始位置> < 结束位置>, 获得值的范围,类似java中的substring
  • setrange < key> < 起始位置> < value> 用 < value> 覆写< key>所储存的字符串值,从< 起始位置>开始(索引从0开始)。
  • setex < key> < 过期时间> < value> 设置键值的同时,设置过期时间,单位秒。
  • getset < key> < value> , 以新换旧,设置了新值同时获得旧值

5. list

  • 一句话:list 类型, 保存多个数据,底层使用双向链表存储结构实现

5.1 list存储结构示意图

  • 双向链表示意图

    image-20241208152747361

  • 解图

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

  • 底层是个双向链表,对两端的操作性能高,通过索引下标的操作中间的节点性能较差

5.2 list常用指令和演示

  • lpush/rpush < key> < value1> < value2> < value3> …. 从左边/右边插入一个或多个值
  • lpop/rpop < key>从左边/右边吐出一个值
  • rpoplpush < key1> < key2>从< key1>列表右边吐出一个值,插到< key2>列表左边
  • lrange < key> < start> < stop> 按照索引下标获得元素(从左到右)
  • 示例:
    • lrange mylist 0 -1 0 左边第一个,-1右边第一个,(0 -1表示获取所有)
  • lindex < key> < index>按照索引下标获得元素(从左到右)
  • llen < key>获得列表长度
  • linsert < key> before < value> < newvalue>在< value>的前面插入< newvalue>插入值
  • linsert < key> after< value> < newvalue>在< value>的后面插入< newvalue>插入值
  • lrem < key> < n> < value>从左边删除 n 个 value(从左到右)
  • lset< key> < index> < value>将列表 key 下标为 index 的值替换成 value

5.3 list最佳实践

  • redis 应用于具有操作先后顺序的数据控制
  • 应用场景
  • 系统通知,按照时间顺序展示,将最近的通知列在前面
  • 其它,比如微信的最近转发,微博的最新关注等.

6. set

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

6.1 set常用指令和使用

6.1.1 set操作指令示意图

image-20241208160351280

6.1.2 操作指令
  • sadd ….. 将一个或多个 member 元素加入到集合 key 中,已经存在的member 元素将被忽略
  • smembers 取出该集合的所有值。
  • sismember 判断集合是否为含有该值,有 1,没有 0
  • scard返回该集合的元素个数。
  • srem …. 删除集合中的某个元素。
  • spop 随机从该集合中吐出一个值。
  • srandmember 随机从该集合中取出n个值。不会从集合中删除 。
  • smove 把集合中一个值从一个集合移动到另一个集合
  • sinter 返回两个集合的交集元素。
  • sunion 返回两个集合的并集元素。。
  • sdiff 返回两个集合的差集元素(key1中的,不包含key2中的)

7.hsah

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

7.1 Redishash 存储结构简单示意图

image-20241208161737445

7.2 hash常用指令和使用

  • hset 集合中的 键赋值
  • hget 集合取出 value
  • hmset … 批量设置 hash 的值
  • hmget … 批量取出 hash 的 filed 值
  • hexists查看哈希表 key 中,给定域 field 是否存在
  • hkeys 列出该 hash 集合的所有 field
  • hvals 列出该 hash 集合的所有 value
  • hincrby 为哈希表 key 中的域 field 的值加上增量 1-1
  • hsetnx 将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在

8.有序集合Zset(sorted set)

8.1 简介

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

8.2 sorted set 指令示意图- 案例蜀国五虎将

image-20241208164207363

8.3 sorted set常用指令和使用

  • zadd … 将一个或多个 member 元素及其 score 值加入到有序集 key 当中。
  • zrange [WITHSCORES] 返回有序集 key 中,下标在之间的元素,带WITHSCORES,可以让分数一起和值返回到结果集
  • zscore 返回有序集 key 中,成员 member 的 score 值
  • zrangebyscore key min max [withscores] [limit offset count] 返回有序集 key 中,所有 score 值介于min 和 max 之间(包括等于 min 或 max)的成员。有序集成员按 score 值递增(从小到大)次序排列。
  • zrevrangebyscore key max min [withscores] [limit offset count] 同上,改为从大到小排列。
  • zincrby 为元素的score加上增量
  • zrem 删除该集合下,指定值的元素
  • zcount 统计该集合,分数区间内的元素个数
  • zrank 返回该值在集合中的排名,从0开始。

六、Redis配置

1.配置文档

  • 配置文档参考网址:https://www.cnblogs.com/nhdlb/p/14048083.html#_label0

2.常规配置

  • 设置密码
  • daemonize
  • loglevel
  • logfile
  • 设定库的数量

3.Units单位

  • 如图

    image-20241208170747343

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

  • 不区分大小写

4.#INCLUDES

  • 如图

    image-20241208170849589

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

5. #NETWORK

  • bind

    image-20241208171005220

  • 默认情况bind=127.0.0.1 只能接受本机的访问请求

  • 如果服务器是需要远程访问的,需要将其注释掉

  • 启动redis, 查看当前允许连接的情况

    image-20241208171121012

  • 注销bind, 重新启动redis, 再查看当前允许连接的情况

    image-20241208171249553

  • protected-mode

  • 如图

    image-20241208171408183

  • 默认是保护模式

  • 如果服务器是需要远程访问的, 需要将yes 设置为 no

  • port

  • 如图

    image-20241208171519504

  • Redis 服务默认端口 6379

  • timeout

  • 如图

    image-20241208171626054

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

  • tcp-keepalive

  • 如图

    image-20241208171709905

  • tcp-keepalive 是对访问客户端的一种心跳检测,每隔n秒检测一次, 单位为秒

  • 如果设置为0,则不会进行Keepalive检测,建议设置成60

  • 为什么需要心跳检测机制

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

6. #GENERAL 通用

  • daemonize

  • 如图

    image-20241208172057184

  • 是否为后台进程,设置为yes

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

  • pidfile

  • 如图

    image-20241208172939109

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

  • loglevel

  • 如图

    image-20241208190423325

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

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

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

  • 如图

    image-20241208190625942

  • logfile “” 就是说,默认为控制台打印,并没有日志文件生成

  • 可以为redis.conf 的 logfile 指定配置项

    image-20241208191117691

  • databases 16

  • 如图

    image-20241208191157648

  • 设定库的数量 默认16,默认数据库为0号

  • 可以使用SELECT命令在连接上指定数据库id

7. #SECURITY 安全

  • 设置密码

  • 如图

    image-20241208191400603

  • 永久设置, 需要在配置文件中进行设置

    image-20241208191526117

  • 命令行设置密码

  • 如图

    image-20241208191943987

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

  • 永久设置,需要在配置文件中进行设置

8.#LIMITS 限制

  • maxclients

  • 如图

    image-20241208192113139

  • 设置redis同时可以与多少个客户端进行连接

  • 默认情况下为10000个客户端。

  • 如果达到了此限制,redis会拒绝新的连接请求,并且向这些连接请求方发出"maxnumber of clients reached”

  • maxmemory

  • 如图

    image-20241208192258585

  • 在默认情况下, 对 32位 实例会限制在 3GB, 因为 32 位的机器最大只支持 4GB 的内存,而系统本身就需要一定的内存资源来支持运行,所以 32 位机器限制最大 3GB 的可用内存是非常合理的,这样可以避免因为内存不足而导致 Redis 实例崩溃

  • 在默认情况下, 对于 64 位实例是没有限制

  • 当用户开启了 redis.conf 配置文件的 maxmemory 选项,那么 Redis 将限制选项的值不能小于 1MB

  • 对maxmemory设置的建议

    • Redis 的maxmemory设置取决于使用情况, 有些网站只需要32MB,有些可能需要12GB。
    • maxmemory只能根据具体的生产环境来调试,不要预设一个定值,从小到大测试,基本标准是不干扰正常程序的运行。
    • Redis 的最大使用内存跟搭配方式有关,如果只是用Redis做纯缓存,64-128M对一般小型网站就足够了
    • 如果使用Redis做数据库的话,设置到物理内存的1/2到3/4左右都可以
    • 如果使用了快照功能的话,最好用到50%以下,因为快照复制更新需要双倍内存空间,如果没有使用快照而设置redis缓存数据库,可以用到内存的80%左右,只要能保证Java、NGINX 等其它程序可以正常运行就行了
  • maxmemory-policy

  • 如图

    image-20241208192626157

  • policy 一览

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

  • 如图

    image-20241208192755171

  • 设置样本数量,LRU算法和最小TTL算法都并非是精确的算法,而是估算值,所以你可以设置样本的大小,redis默认会检查这么多个key并选择其中LRU的那个

  • 一般设置3到7的数字,数值越小样本越不准确,但性能消耗越小

七、订阅和发布

1.发布订阅是什么

  • 一句话:Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息
  • Redis 客户端可以订阅任意数量的频道

1.1 一图胜千言

  • 客户端订阅频道示意图

    image-20241208193835508

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

    image-20241208193849178

2.如何理解发布和订阅模式

2.1 任务队列

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

2.2 如何理解

  • 可以这么简单的理解:
  • Subscriber:收音机,可以收到多个频道,并以队列方式显示
  • Publisher:电台,可以往不同的FM频道中发消息
  • Channel:不同频率的FM频道
  • 从Pub/Sub 的机制来看,它更像是一个广播系统,多个订阅者(Subscriber)可以订阅多个频道(Channel),多个发布者(Publisher)可以往多个频道(Channel)中发布消息。

3.发布订阅模式分类

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

  • 主要应用:通知、公告

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

  • 示意图

    image-20241208194143537

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

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

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

  • 示意图

    image-20241208194250376

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

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

  • 主要应用:群聊、聊天

  • 示意图

    image-20241208194347851

4.命令行实现发布和订阅

4.1 发布订阅操作

  • PUBLISH channel msg 将信息 message 发送到指定的频道 channel
  • SUBSCRIBE channel [channel …] 订阅频道,可以同时订阅多个频道
  • UNSUBSCRIBE [channel …] 取消订阅指定的频道, 如果不指定频道,则会取消订阅所有频道
  • PSUBSCRIBE pattern [pattern …]
  • 订阅一个或多个符合给定模式的频道,每个模式以 * 作为匹配符,比如 it 匹配所 有以 it 开头的频道(it.news 、it.blog 、it.tweets 等等), news. 匹配所有 以 news. 开头的频道(news.it 、 news.global.today 等等),诸如此类
  • PUNSUBSCRIBE [pattern [pattern …]] 退订指定的规则, 如果没有参数则会退订所有规则

4.2 快速入门

  • 在客户端中订阅channel1频道

    image-20241208195151206

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

  • 返回的2, 是订阅者数量

    image-20241208195242354

  • 发布的消息没有持久化

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

    image-20241208195339729

八、Jedis

1. API 文档

  • 在线文档:

2. Jedis介绍

2.1 Jedis 工作示意图

  • Java 程序操作Redis的工具

  • 示意图

    image-20241208200252139

3.Jedis操作Redis数据

3.1 快速入门

  • 创建Maven项目

  • 在pom.xml文件中添加依赖

    <dependencies>
          <!-- 引入Jedis依赖 -->
          <dependency>
              <groupId>redis.clients</groupId>
              <artifactId>jedis</artifactId>
              <version>3.2.0</version>
          </dependency>
     </dependencies>
    
  • Jedis操作Redis数据库

    package com.leon;
    
    import org.junit.Test;
    import redis.clients.jedis.Jedis;
    
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.Set;
    
    /**
    
  • ClassName:studyjedis

  • Package:com.leon

  • Description:
    *

  • @Author: leon–>ZGJ

  • @Create: 2024/12/8 20:06

  • @Version: 1.0
    */
    public class StudyJedis {

    @Test
    public void testJedis() {

      Jedis jedis = new Jedis("192.168.180.130", 6379);
    
      //jedis.auth("leon");
    
      String pong = jedis.ping();
    
      System.out.println(pong);
    

    }

    @Test
    public void testKey(){

      Jedis jedis = new Jedis("192.168.180.130", 6379);
    
      jedis.set("name", "leon");
    
      String name = jedis.get("name");
    
      System.out.println(name);
    

    }

    @Test
    public void testString(){

      Jedis jedis = new Jedis("192.168.180.130", 6379);
    
      jedis.set("k1","jack");
    
      String value = jedis.get("k1");
    
      Long strlen = jedis.strlen("k1");
    
    
      System.out.println(value);
    
      System.out.println(strlen);
    

    }

    @Test
    public void testList(){

      Jedis jedis = new Jedis("192.168.180.130", 6379);
    
      jedis.lpush("list_jedis","a","b","c","d");
    
      List<String> listJedis = jedis.lrange("list_jedis", 0, -1);
    
      for (String listJedi : listJedis) {
          System.out.println("listJedis:"+listJedi);
      }
    

    }

    @Test
    public void testSet(){

      Jedis jedis = new Jedis("192.168.180.130", 6379);
    
      jedis.sadd("set_jedis","tom","lihua","jack","jerry");
    
      Set<String> setJedis = jedis.smembers("set_jedis");
    
      for (String setJedi : setJedis) {
          System.out.println("setJedis:"+setJedi);
      }
    

    }

    @Test
    public void testHash(){

      Jedis jedis = new Jedis("192.168.180.130", 6379);
    
      jedis.hset("hash_jedis","name","jack");
      jedis.hset("hash_jedis","age","25");
    
      Map<String,String> map = new HashMap<>();
    
      map.put("sex","男");
      map.put("hobby","篮球");
    
      jedis.hset("hash_jedis",map);
    
      Map<String, String> hashJedis = jedis.hgetAll("hash_jedis");
    
      for (String s : hashJedis.keySet()) {
          System.out.println(s + "=:=" + hashJedis.get(s));
      }
    

    }

    @Test
    public void testZset(){

      Jedis jedis = new Jedis("192.168.180.130", 6379);
    
    
      jedis.zadd("zset_jedis",1,"关羽");
      jedis.zadd("zset_jedis",2,"张飞");
      jedis.zadd("zset_jedis",3,"赵云");
      jedis.zadd("zset_jedis",4,"马超");
      jedis.zadd("zset_jedis",5,"黄忠");
    
      Set<String> zsetJedis = jedis.zrange("zset_jedis", 0, -1);
    
      for (String zsetJedi : zsetJedis) {
          System.out.println("zsetJedis:"+zsetJedi);
      }
    

    }

    }

九、Spring Boot2整合Redis

1.具体实现

1.1 创建一个Maven项目

1.2 在pom.xml文件中添加相关依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.6</version>
        <relativePath/> <!-- lookup parent from repository-->
    </parent>

    <groupId>com.leon.living</groupId>
    <version>1.0-SNAPSHOT</version>
    <name>redis_springboot</name>
    <artifactId>springboot-redis</artifactId>

    <description>Spring Boot integration with Redis</description>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <!-- 说 明 : 如 果 这 里 是 spring-boot-start 就 改 成 如 下
           spring-boot-start-web-->
            <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>

        <!-- jackson 依赖-->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.13.2.2</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>

1.3 创建配置文件application.properties

#Redis 服务器地址
spring.redis.host=192.168.180.130
#Redis 服务器连接端口
spring.redis.port=6379
#Redis 如果有密码,需要配置, 没有密码就不要写
#spring.redis.password=leon
#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

1.4 redis 配置类

  • 是对要使用的RedisTemplate bean对象的配置, 可以理解成是一个常规配置
  • 与以前学习过的JdbcTemplate,设计理念类似
  • 如果不配置,springboot 会使用默认配置, 这个默认配置, 会出现一些问题, 比如: redisTemplate 的 key 序列化等, 问题所以通常我们需要配置
package com.leon.redis.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

/**
 * ClassName:RedisConfig
 * Package:com.leon.redis.config
 * Description:
 *
 * @Author: leon-->ZGJ
 * @Create: 2024/12/8 20:54
 * @Version: 1.0
 */
// EnableCaching注解开启缓存功能
@EnableCaching
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        // 创建 RedisTemplate 对象
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 输出 RedisTemplate 对象
        System.out.println("template=>" + template);
        // 设置 RedisConnectionFactory
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        // 设置 Jackson2JsonRedisSerializer
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        // 设置 ObjectMapper
        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 对象
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        // 设置 Jackson2JsonRedisSerializer
        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();
        // 创建 CacheManager 对象
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        // 返回 CacheManager 对象
        return cacheManager;
    }

}

1.5 控制器提供API接口

package com.leon.redis.controller;

import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.List;

/**
 * ClassName:RedisController
 * Package:com.leon.redis.controller
 * Description:
 *
 * @Author: leon-->ZGJ
 * @Create: 2024/12/9 14:54
 * @Version: 1.0
 */
@RestController
@RequestMapping("/redisTest")
public class RedisController {

    @Resource
    private RedisTemplate redisTemplate;

    @RequestMapping("/list")
    public String list() {

        ListOperations listOperations = redisTemplate.opsForList();

        listOperations.leftPush("list","tom");
        listOperations.leftPush("list","jerry");

        listOperations.rightPush("list","alice");

        List list = listOperations.range("list", 0, -1);

        return list.toString();


    }

    @RequestMapping("/test")
    public String test() {

        ValueOperations valueOperations = redisTemplate.opsForValue();

        Integer k4 = (Integer) valueOperations.get("k4");
        String hh = (String) valueOperations.get("hh");

        return hh;


    }

}

1.6 注意事项和细节

  • 如果没有提供RedisConfig 配置类 ,springboot 会使用默认配置, 也可以使用

  • 如果没有提供RedisConfig 配置类 ,springboot 会使用默认配置, 但是会存在问题,比如redisTemplate 模糊查找key数据为空。

    image-20241209151045531

    image-20241209151015583

    image-20241209151001713

  • 报错:Unrecognized token 'beijing': was expecting ('true', 'false' or 'null')

    image-20241209151306945

  • 原因:看报错,是jason转换异常,实际上是因为redisTemplate在做数据存储的时候会把存 储的内容序列化,所以,redisTemplate读取的时候也会反序列化,而在redis客户端 set 的时候并不会做序列化,因此set的进去的值在用redisTemplate读的时候就会报类 型转换异常了

  • 解决方案

    • 最简单的就是用程序重新set一遍即可

十、Redis持久化-RDB

1. 官方资料

  • 在线文档:https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/

2. 持久化方案

  • RDB(Redis DataBase)
  • AOF(Append Of File)

3. RDB是什么?

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

4. RDB持久化流程

4.1 RDB及其执行流程

image-20241228102508993

  • 图解
  • redis 客户端执行bgsave命令或者自动触发bgsave命令。
  • 主进程判断当前是否已经存在正在执行的子进程,如果存在,那么主进程直接返回。
  • 如果不存在正在执行的子进程,那么就fork一个新的子进程进行持久化数据,fork过程是阻塞的,fork操作完成后主进程即可执行其他操作。
  • 子进程先将数据写入到临时的rdb文件中,待快照数据写入完成后再原子替换旧的rdb文件。
  • 同时发送信号给主进程,通知主进程rdb持久化完成,主进程更新相关的统计信息。
  • 小结
  • 整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能。
  • 如果需要进行大规模数据的恢复, 且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。
  • RDB 的缺点是最后一次持久化后的数据可能丢失。
    • 解读
    • 如果你是正常关闭Redis, 仍然会进行持久化, 不会造成数据丢失。
    • 如果是Redis 异常终止/宕机, 就可能造成数据丢失。

4.2 Fork和Copy-On-Write

  • Fork 的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、 程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程.
  • 在Linux 程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会 exec 系统调用,出于效率考虑,Linux中引入了"写时复制技术 即:copy-on-write”,.
  • 参考网址:https://blog.csdn.net/Code_beeps/article/details/92838520
  • 一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。

5. RDB配置

5.1 dump.rdb文件

  • 介绍

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

    image-20241228103504269

  • 如何配置

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

    • 也就是你在哪个目录下执行了,redis-server 配置文件 指令,在哪个目录下就会生成指定的dump.rdb文件,这也为什么有的时候发现Redis中没有之前设置的key,所以建议修改到指定目录下,以防失去因为在不同的目录下启动Redis,导致数据丢失。
  • 如图

    image-20241228103933472

    • 测试在/usr/local/bin目录下启动Redis,此时的 ./代表的就是/usr/local/bin目录,如果在/root/目录下启动,此时的 ./ 就代表/root/目录了,这需要注意。

    • 此时目录中没有dump.rdb文件。

      image-20241228104824523

    • 在该目录下启动Redis。

      image-20241228104931690

    • 为了快速见到效果,直接关闭Redis。

      image-20241228105014415

    • 然后就会发现/usr/local/bin目录下生成了dump.rdb文件。

      image-20241228105113033

  • 为了避免数据丢失,修改一下redis.conf配置文件,让其保存在指定位置,修改成指定保存到,root目录下。

    image-20241228105628141

5.2 相关配置和参数以及操作

  • 默认快照配置

  • 如图

    image-20241228105917684

  • 注意理解这个时间段的概念。

    image-20241228105959743

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

  • save VS bgsave

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

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

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

    • 工具地址:https://tool.lu/timestamp/
  • flushall

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

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

    image-20241228112458089

    image-20241228112443899

    image-20241228112425712

  • Save

  • 格式:save 秒钟 写操作次数, 如图。

    image-20241228112713670

  • RDB是整个内存的压缩过的Snapshot,RDB的数据结构,可以配置复合的快照触发条件。

    • 禁用: 给save传入空字符串, 可以看文档。
  • stop-writes-on-bgsave-error

  • 如图

    image-20241228112841743

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

  • rdbcompression

  • 配置如图

    image-20241228112934164

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

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

  • rdbchecksum

  • 配置如图

    image-20241228113110016

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

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

  • 动态停止RDB

  • 动态停止RDB:redis-cli config set save " "

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

  • 如果想永久禁用RDB,需要在配置文件中给 save 设置一个空字符串。如:save ""

    image-20241228113619991

6. RDB备份和恢复

  • 关于RDB备份和恢复
  • Redis可以充当缓存,对项目进行优化, 因此重要/敏感的数据建议在Mysql 要保存一份。
  • 从设计层面来说,Redis的内存数据,都是可以重新获取的(可能来自程序,也可能来自Mysql)。
  • 因此我们这里说的备份&恢复主要是给大家说明一下 Redis启动时, 初始化数据是从dump.rdb 来的, 这个机制。
  • 演示
  • config get dir 查询 rdb 文件的目录。
  • 将dump.rdb 进行备份,如果有必要可以写shell脚本来定时备份 [参考Linux课程定时备份Mysql数据库,视频地址 https://www.bilibili.com/video/BV1Sv411r7vd?p=105 ] ,这里老师简单处理。

7. RDB持久化小结

  • 优势

  • 适合大规模的数据恢复。

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

  • 节省磁盘空间。

  • 恢复速度快。

    image-20241228200138086

  • 劣势

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

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

十一、Redis持久化-AOF

1. 官方资料

  • 在线文档 :https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/

2. AOF是什么?

  • AOF(Append Only File)。
  • 以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(比如 set/del 操作会记录, 读操作get不记录)。
  • 只许追加文件但不可以改写文件。
  • redis 启动之初会读取该文件重新构建数据。
  • redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。

3. AOF持久化流程

  • 持久化流程示意图

    image-20241228201136631

  • 解读上图

  • 客户端的请求写命令会被append追加到AOF缓冲区内。

  • AOF 缓冲区根据AOF持久化策略[always,everysec,no]将操作sync 同步到磁盘的AOF文件中。

  • AOF 文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量。

  • Redis 服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的

4. AOF开启

  • 在redis.conf 中配置文件名称,默认为 appendonly.aof
  • AOF默认是不开启的,如要开启将appendonly 设置成yes
  • 备份文件默认是 appendonly.aof

image-20241228202055857

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

  • AOF和RDB同时开启,系统默认取AOF的数据。

  • 实验,当开启AOF后,Redis从AOF文件取数据。

  • 开启AOF

    image-20241228203954912

  • 重启Redis,连接并设置key

    image-20241228204048860

  • 查看是否有文件生成,查看文件是否发生变化

    image-20241228204201908

    image-20241228204222433

5. AOF 启动/修复/恢复

  • 基本说明

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

  • 正常恢复

  • 修改默认的appendonlyno,改为yes。

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

  • 恢复:重启redis然后重新加载。

  • 异常恢复

  • 如遇到AOF文件损坏,通过/usr/local/bin/redis-check-aof--fix appendonly.aof 进行恢复。

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

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

  • 实验

  • 设置数据。

    image-20241228205444400

  • 在appendonly.aof文件最后一行添加一行k。

    image-20241228205511753

  • 重启Redis,然后使用客户端连接,发现失败。

    image-20241228205541629

  • 修复appendonly.aof文件,然后重启Redis并且使用客户端连接。

image-20241228205611910

6. 同步频率

  • 配置位置

    image-20241228210012690

  • 解读

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

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

  • appendfsync no redis 不主动进行同步,把同步时机交给操作系统

  • 参考网址:https://baijiahao.baidu.com/s?id=1740774723808931509&wfr=spider&for=pc

7. Rewrite 压缩

  • rewrite 重写介绍

  • AOF 文件越来越大,需要定期对AOF文件进行重写达到压缩。

  • 旧的AOF文件含有无效命令会被忽略,保留最新的数据命令 , 比如 setaa1; setab1; set a c1; 保留最后一条指令就可以了。

  • 多条写命令可以合并为一个 , 比如 set a a1 b b1 c c1。

  • AOF 重写降低了文件占用空间。

  • 更小的AOF文件可以更快的被redis加载

  • 重写触发配置

  • 直接调用bgrewriteaof 命令

    image-20241228210538944

  • 自动触发

    image-20241228210636889

    • 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进行重写

8. AOF 持久化小结

  • 优势

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

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

    image-20241228210937653

  • 劣势

  • 比起RDB占用更多的磁盘空间。

  • 恢复备份速度要慢。

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

9. RDB 还是 AOF?

  • 官方文档地址:https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/

    image-20241228211202664

  • 官方推荐两个都启用。

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

十二、Redis-事务-锁机制-秒杀

1. Redis的事务是什么?

  • Redis 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。
  • 事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
  • Redis 事务的主要作用就是串联多个命令防止别的命令插

2. Redis事务三特性

  • 单独的隔离操作
  • 事务中的所有命令都会序列化、按顺序地执行。
  • 事务在执行的过程中,不会被其他客户端发送来的命令请求所打断
  • 没有隔离级别的概念
  • 队列中的命令(指令), 在没有提交前都不会实际被执行
  • 不保证原子性
  • 事务执行过程中, 如果有指令执行失败,其它的指令仍然会被执行, 没有回滚。

3. 事务相关指令Multi、Exec、discard

  • Redis 事务指令示意图

    image-20241228214158352

  • 解读

  • 从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行(类似Mysql的starttransaction 开启事务)。

  • 输入Exec后,Redis会将之前的命令队列中的命令依次执行(类似Mysql 的 commit 提交事务)。

  • 组队的过程中可以通过discard来放弃组队(类似Mysql 的rollback 回顾事务)。

  • Redis事务和Mysql事务本质是完全不同的, 用Mysql的做类似说明, 是为了方便理解

  • 快速入门

  • 请依次向Redis中, 添加三组数据,name-tom age-10 sex-1, 要求使用Redis的事务完成。

    image-20241228214902960

  • 注意事项和细节

  • 组队的过程中, 可以通过discard来放弃组队

    image-20241228214958530

  • 如果在组队阶段报错, 会导致exec失败,那么事务的所有指令都不会被执行。

    image-20241228215105182

    image-20241228215233218

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

    image-20241228215316062

    image-20241228215414892

4. 事务冲突以及解决方案

4.1 先看一个问题

  • 经典的抢票问题
  • 一个请求想购买6
  • 一个请求想购买5
  • 一个请求想购买1

image-20241228215548893

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

4.2 悲观锁

  • 工作示意图

    image-20241228215719442

  • 图解

  • 悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修 改,所以每次在拿数据的时候都会上锁。

  • 这样别人/其它请求想拿这个数据就会block直到它拿到锁。

  • 悲观锁是锁设计理念, 传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

4.3 乐观锁

  • 工作示意图

    image-20241228215852300

  • 图解

  • 乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁。

  • 但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。

  • 乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。

  • 乐观锁是锁设计理念

4.3 watch 和 unwatch

  • watch

  • 基本语法:watch key [key ...]

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

  • 这里可以结合乐观锁机制进行理解。

    image-20241229203422337

    image-20241229203447422

    • 解读事务中断流程
    • A连接和B连接都操作Redis,前提是需要在事务中。
    • 在事务开始前,都watch k1:理解成得到了k1的版本v1.0。
    • A和B都开始multi事务,进行指令组队。
    • A连接,先提交事务exec,这时可以理解Redis底层乐观锁机制,将k1版本set成为v1.1。
    • B连接,再提交事务exec,但是因为B原来获取的k1版本是v1.0,现在k1版本是v1.1。
    • 就会造成事务被打断,所以B连接的事务执行失败。
  • unwatch

  • 基本语法:unwatch

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

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

5. 火车票-抢票

5.1 需求分析和图解

  • 思路分析

  • 一个user只能购买一张票, 即不能复购。

  • 不能出现超购,也是就多卖了。

  • 不能出现火车票遗留问题/库存遗留, 即火车票不能留下。

    image-20241229205203903

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

  • 创建JavaWeb项目, 参照以前讲过搭建JavaWeb项目流程即可

  • 引入相关的jar包 和 jquery

    image-20241229221504870

  • 创建index.jsp

    <%--
    Created by IntelliJ IDEA.
    User: ZGJ
    Author: leon --> ZGJ
    Version: 1.0
    Date: 2024/12/29
    Time: 20:58
    To change this template use File | Settings | File Templates.
    --%>
    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    
    <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN""http://www.w3.org/TR/html4/loose.dtd">
    <html>
    <head>
      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
      <title>Insert title here</title>
      <base href="<%=request.getContextPath() + "/"%>">
    </head>
    <body>
    <h1>北京-成都 火车票 ! 秒杀!</h1>
    <form id="secKillform" action="secKillServlet" enctype="application/x-www-form-urlencoded">
      <input type="hidden" id="ticketNo" name="ticketNo" value="bj_cd">
      <input type="button" id="seckillBtn" name="seckillBtn" value="秒杀火车票【北京-成都】"/>
    </form>
    </body>
    <script type="text/javascript" src="script/jquery/jquery-3.7.1.min.js"></script>
    <script type="text/javascript">
      $(function () {
          $("#seckillBtn").click(function () {
              var url = $("#secKillform").attr("action");
              $.post(url, $("#secKillform").serialize(), function (data) {
                  if (data == "false") {
                      alert("火车票 抢光了:)");
                      $("#seckillBtn").attr("disabled", true);
                  }
              });
          })
      })
    </script>
    </html>
    
  • 创建SecKillRedis.java

    package com.leon.seckill.redis;
    
    import org.junit.Test;
    import redis.clients.jedis.Jedis;
    
    /**
    
  • ClassName:SeckillRedis

  • Package:com.leon.seckill.redis

  • Description:
    *

  • @Author: leon–>ZGJ

  • @Create: 2024/12/29 21:19

  • @Version: 1.0
    */
    public class SecKillRedis {

    @Test
    public void test(){

      Jedis jedis = new Jedis("192.168.180.130",6379);
    
      String ping = jedis.ping();
    
      System.out.println(ping);
    

    }

    /**

    • @param userId 用户id,由后台生成

    • @param ticketId 票id
      */
      public static boolean doSecKill(String userId,String ticketId){

      // 判断用户id和票id是否为空
      if(userId == null || ticketId == null){

        return false;
      

      }

      // 连接redis,获取jedis对象
      Jedis jedis = new Jedis(“192.168.180.130”,6379);

      // 拼接票库存的key
      String stockKey = “sk:“+ticketId+“:ticket”;

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

      // 获取票库存
      String stock = jedis.get(stockKey);

      // 判断票库存是否为空
      if(stock == null){

        System.out.println("秒杀还未开始,请等待");
        // 关闭连接
        jedis.close();
      
        return false;
      

      }

      // 判断该用户是否已经抢过票
      if(jedis.sismember(userKey,userId)){

        System.out.println(userId+"您已经抢过票了,不能重复抢购");
        // 关闭连接
        jedis.close();
      
        return true;
      

      }

      // 判断票库存是否小于等于0
      if(Integer.parseInt(stock) <= 0){

        System.out.println("票已经抢光了,请下次再来");
        // 关闭连接
        jedis.close();
      
        return false;
      

      }

      // 减少票库存,进行秒杀
      jedis.decr(stockKey);
      // 将用户id存放到set集合中
      jedis.sadd(userKey,userId);

      System.out.println(userId+“秒杀成功“);

      // 关闭连接
      jedis.close();

      return true;

      }
      }

  • 创建SecKillServlet.java

    package com.leon.seckill.web;
    
    import com.leon.seckill.redis.SecKillRedis;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.Random;
    
    /**
    
  • ClassName:SecKillServlet

  • Package:com.leon.seckill.web

  • Description:
    *

  • @Author: leon–>ZGJ

  • @Create: 2024/12/29 21:53

  • @Version: 1.0
    */
    public class SecKillServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

      doPost(req, resp);
    

    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

      // 处理秒杀请求
      // 1. 获取秒杀票的编号
      String ticketNo = req.getParameter("ticketNo");
    
      // 碎金生成用户ID
      String userId = new Random().nextInt(10000)+"";
    
      // 进行秒杀
      boolean doSecKill = SecKillRedis.doSecKill( userId,ticketNo);
    
      // 返回秒杀结果
      resp.getWriter().print(doSecKill);
    

    }
    }

  • 配置web.xml文件

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
           version="4.0">
    
      <servlet>
          <servlet-name>SecKillServlet</servlet-name>
          <servlet-class>com.leon.seckill.web.SecKillServlet</servlet-class>
      </servlet>
      <servlet-mapping>
          <servlet-name>SecKillServlet</servlet-name>
          <url-pattern>/secKillServlet</url-pattern>
      </servlet-mapping>
    </web-app>
    
  • 向Redis中设置数据

    image-20241229221820995

  • 测试结果

    image-20241229221857895

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

  • 安装工具ab模拟测试

  • 说明: 工具ab 可以模拟并发发出Http请求, 说明(模拟并发http请求工具还有jemeter, postman,都使用一下, 开阔眼界, 这里老师使用ab工具)。

  • 安装指令:yuminstall httpd-tools (提示: 保证当前linux 是可以联网的)。

  • 如果你不能联网, 可以使用rpm安装, 这里使用yum 方式安装

  • 另外, 使用rpm方式安装我也给小伙伴说明一下, 如下:

    • 先挂载centos安装文件ios, 这个文件, 以前老师给过

      image-20241229222059591

      image-20241229222129085

    • 进入cd /run/media/root/CentOS 7 x86_64/Packages

    • 顺序安装

    • apr-1.4.8-3.el7.x86_64.rpm

    • apr-util-1.5.2-6.el7.x86_64.rpm

    • httpd-tools-2.4.6-67.el7.centos.x86_64.rpm

    • 测试是否安装成功

      image-20241229222251495

  • 在ab指令执行的当前路径下 创建文件 postfile

  • 指令:vi postfile

  • 执行指令 , 注意保证 linux可以访问到Tomcat所在的服务器

    • 先查看Tomcat所在Windows的网络配置情况

      image-20241230103607930

    • 确认Linux 可以ping通Windows

      image-20241230104406932

    • 如果Ping不通, 确认一下Windows防火墙是否关闭

      image-20241230104420513

    • 指令 , 测试前把Redis的数据先重置一下

    • ab-n 1000-c 100-p ~/postfile-T application/x-www-form-urlencoded http://主机IP:8080/seckill/secKillServlet

    • 解读指令

    • ab 是并发工具程序。

    • -n 1000 表示一共发出1000次http请。

    • -c 100 表示并发时100次, 你可以理解1000次请求, 会在10次发送完毕。

    • -p ~/postfile 表示发送请求时, 携带的参数从当前登录用户的用户目录的 postfile文件读取 (这个需要事先要准备好)

    • -T application/x-www-form-urlencoded 就是发送数据的编码是基于表单的 url 编码

      image-20241230105649269

    • ~的含义: https://blog.csdn.net/m0_67401134/article/details/123973115、

    • http://主机地址:8080/seckill/secKillServlet 就 是 请 求 的 url, 注 意 这 里 的IP:port/uri 必须写正确

      image-20241230105722228

    • 查看结果

      image-20241230105745555

      image-20241230105802424

5.4 连接池技术

  • 连接池介绍

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

  • 链接池参数

    • MaxTotal:控制一个pool可分配多少个jedis实例,通过pool.getResource()来获取;如果赋值为-1,则表示不限制。
    • maxIdle:控制一个pool最多有多少个状态为idle(空闲)的jedis实例。
    • MaxWaitMillis:表示当获取一个jedis 实例时,最大的等待毫秒数,如果超过等待时间,则直接抛JedisConnectionException。
    • testOnBorrow:获得一个jedis 实例的时候是否检查连接可用性(ping());如果为true,则得到的jedis实例均是可用的。
  • 使用连接池, 优化连接超时

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

  • 创建JedisPoolUtil.java

    • 线程可见性

    • 简单说一下指令重排

      image-20241230112723802

    package com.leon.seckill.utils;
    
    import redis.clients.jedis.JedisPool;
    import redis.clients.jedis.JedisPoolConfig;
    
    /**
     * ClassName:JedisPoolUtil
     * Package:com.leon.seckill.utils
     * Description:
     *
     * @Author: leon-->ZGJ
     * @Create: 2024/12/30 11:07
     * @Version: 1.0
     */
    public class JedisPoolUtil {
    
        // 单例模式,保证线程安全
        // 静态代码块,在类被加载时执行
        // volatile关键字,保证线程可见性:当一个线程去修改一个共享变量时,另外一个线程可以读取这个修改的值
        // 顺序的一致性:禁止指令重排,保证代码执行顺序的一致性
        private static volatile JedisPool jedisPool = null;
    
        private JedisPoolUtil() {
            // 防止实例化
        }
    
        public static JedisPool getJedisPool() {
    
            if (jedisPool == null) {
                // 同步代码块,保证线程安全
                synchronized(JedisPoolUtil.class){
                    // 双重检查锁定
                    if (jedisPool == null) {
                        JedisPoolConfig config = new JedisPoolConfig();
                        // 设置最大连接数
                        config.setMaxTotal(200);
                        // 设置最大空闲连接数
                        config.setMaxIdle(32);
                        // 设置连接超时时间,单位毫秒
                        config.setMaxWaitMillis(60 * 1000);
                        // 连接池中如果没有可用连接,是否阻塞,默认为true
                        config.setBlockWhenExhausted(true);
                        // 测试连接是否有效
                        config.setTestOnBorrow(true);
                        // 创建Jedis连接池
                        jedisPool = new JedisPool(config, "192.168.180.130", 6379);
                    }
                }
    
    
            }
    
            return jedisPool;
    
        }
    
        // 关闭连接池
        public static void closeJedisPool(JedisPool jedisPool) {
            // 判断连接池是否为空
            if (jedisPool != null) {
                // 关闭连接池
                jedisPool.close();
            }
        }
    }
    
  • 修改SecKillRedis.java

    package com.leon.seckill.redis;
    
    import com.leon.seckill.utils.JedisPoolUtil;
    import org.junit.Test;
    import redis.clients.jedis.Jedis;
    
    /**
     * ClassName:SeckillRedis
     * Package:com.leon.seckill.redis
     * Description:
     *
     * @Author: leon-->ZGJ
     * @Create: 2024/12/29 21:19
     * @Version: 1.0
     */
    public class SecKillRedis {
    
    
        @Test
        public void test(){
    
            Jedis jedis = new Jedis("192.168.180.130",6379);
    
            String ping = jedis.ping();
    
            System.out.println(ping);
    
        }
    
        /**
         * @param userId 用户id,由后台生成
         * @param ticketId 票id
         */
        public static boolean  doSecKill(String userId,String ticketId){
    
            // 判断用户id和票id是否为空
            if(userId == null || ticketId == null){
                return false;
            }
    
            // 连接redis,获取jedis对象
            //Jedis jedis = new Jedis("192.168.180.130",6379);
            // 使用连接池
            Jedis jedis = JedisPoolUtil.getJedisPool().getResource();
    
            // 拼接票库存的key
            String stockKey = "sk:"+ticketId+":ticket";
    
            // 拼接秒杀用户要存放到set集合对应的key,这个set集合可以存放多个userId
            String userKey = "sk:"+ticketId+":user";
    
            // 获取票库存
            String stock = jedis.get(stockKey);
    
            // 判断票库存是否为空
            if(stock == null){
                System.out.println("秒杀还未开始,请等待");
                // 关闭连接
                jedis.close();
    
                return false;
            }
    
            // 判断该用户是否已经抢过票
            if(jedis.sismember(userKey,userId)){
    
                System.out.println(userId+"您已经抢过票了,不能重复抢购");
                // 关闭连接
                jedis.close();
    
                return true;
    
            }
    
    
            // 判断票库存是否小于等于0
            if(Integer.parseInt(stock) <= 0){
    
                System.out.println("票已经抢光了,请下次再来");
                // 关闭连接
                jedis.close();
    
                return false;
            }
    
            // 减少票库存,进行秒杀
            jedis.decr(stockKey);
            // 将用户id存放到set集合中
            jedis.sadd(userKey,userId);
    
            System.out.println(userId+"秒杀成功~~~~");
    
            // 关闭连接
            jedis.close();
    
            return true;
    
    
    
        }
    }
    

5.5 利用Redis事务机制,解决超卖

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

    image-20241230160752278

  • 修改SecKillRedis.java

    public static boolean  doSecKill(String userId,String ticketId){
    
          // 判断用户id和票id是否为空
          if(userId == null || ticketId == null){
              return false;
          }
    
          // 连接redis,获取jedis对象
          //Jedis jedis = new Jedis("192.168.180.130",6379);
          // 使用连接池
          Jedis jedis = JedisPoolUtil.getJedisPool().getResource();
    
          // 拼接票库存的key
          String stockKey = "sk:"+ticketId+":ticket";
    
          // 拼接秒杀用户要存放到set集合对应的key,这个set集合可以存放多个userId
          String userKey = "sk:"+ticketId+":user";
    
          // 监听票库存的key
          jedis.watch(stockKey);
    
          // 获取票库存
          String stock = jedis.get(stockKey);
    
          // 判断票库存是否为空
          if(stock == null){
              System.out.println("秒杀还未开始,请等待");
              // 关闭连接
              jedis.close();
    
              return false;
          }
    
          // 判断该用户是否已经抢过票
          if(jedis.sismember(userKey,userId)){
    
              System.out.println(userId+"您已经抢过票了,不能重复抢购");
              // 关闭连接
              jedis.close();
    
              return true;
    
          }
    
    
          // 判断票库存是否小于等于0
          if(Integer.parseInt(stock) <= 0){
    
              System.out.println("票已经抢光了,请下次再来");
              // 关闭连接
              jedis.close();
    
              return false;
          }
    
          // 开启事务
          Transaction multi = jedis.multi();
          // 减少票库存,进行秒杀
          multi.decr(stockKey);
          // 将用户id存放到set集合中
          multi.sadd(userKey,userId);
          // 执行事务,并返回结果
          List<Object> exec = multi.exec();
    
          // 判断事务是否执行成功
          if(exec == null || exec.size() == 0){
              System.out.println("秒杀失败,请重试");
              // 关闭连接
              jedis.close();
              return false;
    
          }
    
          //// 减少票库存,进行秒杀
          //jedis.decr(stockKey);
          //// 将用户id存放到set集合中
          //jedis.sadd(userKey,userId);
    
          System.out.println(userId+"秒杀成功~~~~");
    
          // 关闭连接
          jedis.close();
    
          return true;
    
    
    
      }
    
  • 重启 Tomcat

  • 重置 Redis相关数据

    image-20241230163216291

  • 执行指令

  • 指令:ab -n 1000 -c 100 -p ~/postfile -T application/x-www-form-urlencoded http://192.168.180.1:8080/secKillServlet

    image-20241230163338500

    image-20241230163351925

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

  • 先重置一下redis的数据

    image-20241230163432168

  • 解读

  • 这里把库存量设的较大 , 为600

  • 执行指令

    • 指令:ab -n 1000 -c 300 -p ~/postfile -T application/x-www-form-urlencoded http://192.168.180.1:8080/secKillServlet

    • 这里将并发数变大 -c 300

  • 执行结果

    image-20241230163731167

  • 解读

    • 可以看到, 剩余票数为543, 并不是0
  • 出现库存遗留问题的分析

    image-20241230163834339

5.7 LUA脚本

5.7.1 LUA介绍
  • Lua 是一个小巧的脚本语言,Lua脚本可以很容易的被C/C++ 代码调用,也可以反过来调用C/C++的函数,Lua并没有提供强大的库,一个完整的Lua解释器不过200k,所以Lua不适合作为开发独立应用程序的语言,而是作为嵌入式脚本语言。
  • 很多应用程序、游戏使用LUA作为自己的嵌入式脚本语言,以此来实现可配置性、可扩展性。
  • 将复杂的或者多步的Redis操作,写为一个脚本,一次提交给redis执行,减少反复 连接redis的次数。提升性能。
  • LUA脚本是类似Redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis 事务性的操作。
  • Redis 的lua脚本功能,只有在Redis2.6以上的版本才可以使用。
  • 通过lua脚本解决争抢问题,实际上是Redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题。
5.7.1 LUA 脚本, 解决库存遗留-思路分析图
  • 一图胜千言

    image-20241230164624624

  • 图解

  • LUA脚本是类似Redis事务,有一定的原子性,不会被其他命令插队,能完成Redis 事务性的操作。

  • 通过lua脚本解决争抢问题,Redis利用其单线程的特性,将请求形成任务队列,从 而解决多任务并发问题。

5.7.3 LUA脚本,解决库存遗留-代码实现
  • 编写lua脚本文件

    local userid=KEYS[1];--获取传入的第一个参数
    local ticketno=KEYS[2];--获取传入的第二个参数
    local stockKey='sk:'..ticketno..:ticket;--拼接stockKey
    local usersKey='sk:'..ticketno..:user;--拼接usersKey
    local userExists=redis.call("sismember",usersKey,userid);--查看在redis的usersKeyset中是否有该用户
    if tonumber(userExists)==1 then
    return 2;--如果该用户已经购买,返回2
    end
    local num=redis.call("get",stockKey);--获取剩余票数
    if tonumber(num)<=0 then
    return 0;--如果已经没有票,返回0
    else
    redis.call("decr",stockKey);--将剩余票数-1
    redis.call("sadd",usersKey,userid);--将抢到票的用户加入set
    end
    return 1--返回1表示抢票成功
    
  • 参考文档:https://blog.csdn.net/qq_41286942/article/details/124161359

  • 创建SecKillRedisByLua.java

    package com.leon.seckill.redis;
    
    import com.leon.seckill.utils.JedisPoolUtil;
    import redis.clients.jedis.Jedis;
    
    /**
    
  • ClassName:SecKillRedisByLua

  • Package:com.leon.seckill.redis

  • Description:
    *

  • @Author: leon–>ZGJ

  • @Create: 2024/12/30 16:55

  • @Version: 1.0
    */
    public class SecKillRedisByLua {

    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";
    

    public static boolean doSecKill(String userId,String ticketNo){

      // 1. 连接redis
      Jedis jedis = JedisPoolUtil.getJedisPool().getResource();
    
      // 2. 将Lua加载到内存中
      String scriptLoad = jedis.scriptLoad(secKillScript);
    
      // 3. 执行Lua脚本,evalsha方法是根据指定的scriptLoad校验码,执行缓存在服务器中的脚本
      Object evalsha = jedis.evalsha(scriptLoad, 2, userId, ticketNo);
    
      // 将返回的结果装换成字符串
      String result = String.valueOf(evalsha);
    
    
      // 根据返回的结果做出相应的处理
      if("2".equals(result)){
    
          System.out.println("你购票已经成功,请勿重复购买");
    
          jedis.close();
    
          return false;
    
      }
    
      if("0".equals(result)){
          System.out.println("秒杀结束,没有库存了");
          jedis.close();
          return false;
      }
    
      if ("1".equals(result)){
          System.out.println("秒杀成功");
          jedis.close();
          return true;
      }
    
      System.out.println("购票失败,请稍后再试");
    
      jedis.close();
    
      return false;
    

    }
    }

  • 修改SecKillServlet.java

    package com.leon.seckill.web;
    
    import com.leon.seckill.redis.SecKillRedis;
    import com.leon.seckill.redis.SecKillRedisByLua;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.Random;
    
    /**
    
  • ClassName:SecKillServlet

  • Package:com.leon.seckill.web

  • Description:
    *

  • @Author: leon–>ZGJ

  • @Create: 2024/12/29 21:53

  • @Version: 1.0
    */
    public class SecKillServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

      doPost(req, resp);
    

    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

      // 处理秒杀请求
      // 1. 获取秒杀票的编号
      String ticketNo = req.getParameter("ticketNo");
    
      // 碎金生成用户ID
      String userId = new Random().nextInt(10000)+"";
    
      // 进行秒杀
      //boolean doSecKill = SecKillRedis.doSecKill( userId,ticketNo);
    
      boolean doSecKill = SecKillRedisByLua.doSecKill(userId, ticketNo);
    
      // 返回秒杀结果
      resp.getWriter().print(doSecKill);
    

    }
    }

  • 完成测试

  • 重启抢票程序-Tomcat

  • 重置Redis数据

    image-20241230195229657

  • 执行指令

    • 指令:ab -n 1000 -c 300 -p ~/postfile -T application/x-www-form-urlencoded http://192.168.180.1:8080/secKillServlet

      image-20241230195400811

      image-20241230195546624

十三、主从复制

1. 主从复制介绍

1.1 一图胜千言

  • Redis 主从复制的示意图

    image-20241230201007956

  • 图解

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

  • Master 以写为主,Slaver 以读为主。

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

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

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

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

2. 搭建一主多从

  • 需求说明

  • 搭建主从复制结构。

  • 这里搭建 一主二从即可, 其它slaver 可以依此完成。

  • 实现

  • 创建目录, 并拷贝redis.conf 到 /hspredis

    image-20241230202506405

    image-20241230202641759

  • vi /hspredis/redis.conf , 进行如下设置

    image-20241230203419690

    image-20241230203445416

  • 创建3个文件/hspredis/redis6379.conf /hspredis/redis6380.conf /hspredis/redis6381.conf 并编辑。

    include /studyredis/redis.conf
    pidfile /var/run/redis_6379.pid
    port 6379
    dbfilename dump6379.rdb
    

    image-20241230203807442

    image-20241230204324075

    image-20241230204350566

  • 启动三台redis服务器
    image-20241230204240123

  • 连接到3个Redis服务,inforeplication 打印主从复制的相关信息, 如图, 这时三台Redis 都是 Master.

    image-20241230204809963

    image-20241230204836745

    image-20241230204855708

  • 将6380 和 6381 配置成slaver,6379 作为主机

    • 使用slaver指令,成为某个实例的从服务器.

    • 指令说明:slaveof < master_ip> < master_port>

      image-20241230205647962

      image-20241230205702419

  • 在主机上写,在从机上可以读取数据, 如果你在从机上写入数据,就报错。

    image-20241230205840435

    image-20241230205853904

    image-20241230205820995

3. 主从复制-原理

3.1 原理示意图

image-20241230210254665

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

3.2 一主二仆

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

  • 测试

    • 关闭6381从服务

      image-20241230211140823

    • 主服务6379设置新数据

      image-20241230211201106

    • 重启6381服务

      image-20241230211331705

    • 输入info replication指令,发现又变为主机了

      image-20241230211414613

    • 执行slaveof指令,发现获取到最新数据了

      image-20241230211536050

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

  • 实验

    • 关闭主服务。

      image-20241230211914808

    • 查看从服务还是没有变化,显示主服务已经死了。

      image-20241230211946722

    • 重启主服务。

      image-20241230212023532

    • 查看从服务,显示主服务是运行状态。

      image-20241230212039252

      image-20241230212058092

3.3 薪火相传

  • 示意图

    image-20241230212359442

  • 图解

  • 上一个Slave可以是下一个slave的Master,Slave同样可以接收其他 slaves的连接和同 步请求,那么该slave作为了链条中下一个的master, 可以有效减轻master的写压力,去中心化降低风险。

  • 用 slaveof < master_ip> < master_port>。

  • 风险是一旦某个slave宕机,后面的slave都没法同步。

  • 主机挂了,从机还是从机,无法写数据了。

  • 实验

  • 将6381 的主机设置为 6380, 他的数据同步就是从 6380获取

    image-20241230212633927

  • 查看 6380 和 6379,发现6380还是你从机,6379还是主机,但是6376显示只有一个从机

    image-20241230212941755

    image-20241230213041103

3.4 反客为主

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

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

  • 实验

  • 关闭主机6379

    image-20241230213441156

  • 将从机6380升为主机

    image-20241230213545198

  • 6380可以写入数据了

    image-20241230213608235

3.5 哨兵模式

3.5.1 实例演示
  • 工作示意图

    image-20241231153857744

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

  • 实验

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

  • 创建 /hspredis/sentinel.conf , 名字不能乱写, 按照指定的来

    sentinel monitor redis_master 127.0.0.1 6379 1
    

    image-20241231154628970

  • 说明

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

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

    • 启动哨兵, 注意看哨兵的端口是 26379

    • 启动指令redis-sentinel sentinel.conf

      image-20241231154756359

    • 当主机挂掉,从机选举中产生新的主机

      image-20241231154912321

      image-20241231155014587
      image-20241231155056177

      image-20241231155119046

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

      image-20241231155235279

  • 注意事项和细节

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

    image-20241231155331920

  • 图解

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

十四、集群

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

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

  • 容量不够,redis如何进行扩容?

  • 并发写操作, redis如何分摊?

  • 主从模式,薪火相传模式,主机宕机,会导致ip地址发生变化,应用程序中配置需要修 改对应的主机地址、端口等信息。

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

    image-20241231155845934

  • 图解

    • 客户端请求先到代理服务器, 由代理服务器进行请求转发到对应的业务处理服务器。
    • 为了高可用性, 代理服务、A服务、B服务、C服务都需要搭建主从结构(至少是一主一从),这样就需求搭建至少8台服务器, 这种方案的缺点是: 成本高,维护困难, 如果是一主多从,成本就会更高。
  • redis3.0 提供解决方案-无中心化集群配置

    image-20241231160029834

  • 图解

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

2. 集群介绍

  • Redis 集群实现了对Redis的水平扩容,即启动N个redis节点,将整个数据库分布存储在这N个节点中,每个节点存储总数据的1/N。
  • Redis 集群通过分区(partition)来提供一定程度的可用性(availability): 即使集群中 有一部分节点失效或者无法进行通讯, 集群也可以继续处理命令请求。

3. Redis集群搭建

3.1 实例演示

  • 需求说明/图解

    image-20241231160333942

  • 搭建步骤演示

  • 将rdb、aof文件都删除掉

    image-20241231160616015

    image-20241231160630020

  • redis cluster 配置修改

    cluster-enabled yes 打开集群模式
    cluster-config-file nodes-6379.conf 设定节点配置文件名
    cluster-node-timeout 15000 设定节点失联时间,超过该时间(毫秒),集群自动进行主从切换
    
  • vi /hspredis/redis6379.conf , 删除不必要的内容, 增加 cluster 配置, 文件最后内容,如图

    image-20241231161150066

  • 删除6380,6381配置文件

    image-20241231161032545

  • 重新复制五份配置文件

    image-20241231161445424

  • 使用查找替换修改另外5个文件

    • 指令: :%s/6379/6380

    • 使用:vi redis6380.conf

      image-20241231161713173

      image-20241231161728247

    • 其它几个文件以此操作即可, 操作的时候,一定要小心, 最后建议再检查一下

  • 启动6个Redis服务

    image-20241231162148732

    image-20241231162236614

  • 将六个节点合成一个集群

    • 先进入到你安装Redis时,使用的源文件目录。

    • 每个人都不一定相同。

    • 主要是要使用它的指令redis-cli。

    • 同时还需要一个ruby环境,redis6已经有了。

      image-20241231162625730

  • 进群指令

    redis-cli --cluster create --cluster-replicas 1 192.168.180.130:6379 192.168.180.130:6380 192.168.180.130:6381 192.168.180.130:6389 192.168.180.130:6390 192.168.180.130:6391
    
  • 注意事项和细节

    • 组合之前,请确保所有redis实例启动后,nodes-xxxx.conf文件都生成正常。
    • 此处不要用127.0.0.1,请用真实IP地址,在真实生产环境,IP都是独立的。
    • replicas 1 采用最简单的方式配置集群,一台主机,一台从机,正好三组。
    • 搭建集群如果没有成功, 把sentinel进程kill掉, 再试一下。
    • 分析主从对应关系。

    image-20241231163209671

    image-20241231163223802

    image-20241231163237707

    image-20241231163247307

  • 集群方式登录

    • 指令:redis-cli -c -p 6379

    • 指令:cluster nodes 命令查看集群信息, 主从的对应关系, 主要看标注的颜色

      image-20241231163649503

  • 注意事项和细节

    • 一个集群至少要有三个主节点。
    • 选项–cluster-replicas 1 表示我们希望为集群中的每个主节点创建一个从节点。
    • 分配原则:尽量保证主服务器和从服务器各自运行在不同的IP地址(机器), 防止机器故 障导致主从机制失效, 高可用性得不到保障。

4. Redis进群使用

4.1 么是slots

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

    image-20241231164114272

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

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

    image-20241231164236743

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

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

4.2 在集群中录入值

  • 在redis 每次录入、查询键值,redis都会计算出该key应该送往的插槽,如果不是该客户 端对应服务器的插槽,redis会告知应前往的redis实例地址和端口。

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

  • 如 redis-cli-c–p6379 登入后,再录入、查询键值对可以自动重定向。

  • 当进行:set k1 200

  • k1经过计算,需要录入到12706这个插槽。

  • 而这个12706插槽在6381主机,所以自动化切换到6381。

    image-20241231164633090

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

  • 因为k2,k3,k4可能分布在不同的slot

    image-20241231165003917

  • 可以通过{}来定义组的概念,从而使key中{}内相同内容的键值对放到一个slot中去。

    image-20241231165054251

4.3 查询集群中的值

  • 指令:CLUSTER KEYSLOT < key> 返回 key对应的slot值。

  • 指令:CLUSTER COUNTKEYSINSLOT < slot>返回 slot 有多少个key。

  • 指令:CLUSTER GETKEYSINSLOT < slot> < count>返回 count 个 slot 槽中的键

    image-20241231170057819

5. Redis集群故障恢复

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

    image-20241231170339246

    image-20241231170328362

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

    image-20241231170452828

image-20241231170513516

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

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

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

  • redis.conf 中的参数 cluster-require-full-coverage。

    image-20241231170755744

6. 集群的Jedis 开发

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

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

  • 实验

  • 开放所有端口

    image-20241231171206454

  • 创建JedisClusterTest.java类

    package com.leon;
    
    import org.junit.Test;
    import redis.clients.jedis.HostAndPort;
    import redis.clients.jedis.JedisCluster;
    
    import java.util.HashSet;
    import java.util.Set;
    
    /**
     * ClassName:JedisClusterTest
     * Package:com.leon
     * Description:
     *
     * @Author: leon-->ZGJ
     * @Create: 2024/12/31 17:13
     * @Version: 1.0
     */
    public class JedisClusterTest {
    
    
        @Test
        public void test() {
    
            //解读
            //1. 这里set 也可以加入多个入口
            //2. 因为没有配置日志, 会有提示, 但是不影响使用
            //3. 如果使用集群,需要把集群相关的端口都打开,
            //否则会报 Nomorecluster attempts left.
            //4. JedisCluster 看源码有多个构造器, 也可以直接传入一个HostAndPort
            /**
             * HostAndPort hostAndPort = new HostAndPort("192.168.198.130", 6379);
             *         JedisCluster jedisCluster = new JedisCluster(hostAndPort);
             */
            Set<HostAndPort> set = new HashSet<>();
            // 添加集群节点,IP地址和端口
            set.add(new HostAndPort("192.168.180.130", 6379));
    
            // 创建JedisCluster对象,将set作为参数传入
            JedisCluster jedisCluster = new JedisCluster(set);
    
            jedisCluster.set("name", "leon");
            String value = jedisCluster.get("name");
            System.out.println(value);
    
    
    
        }
    
    }
    
  • 完成测试

    image-20241231171753834

7. Redis 集群的优缺点

7.1 优点

  • 实现扩容。
  • 分摊压力。
  • 无中心配置相对简单。

7.2 缺点

  • 多键操作是不被支持的。
  • 多键的Redis事务是不被支持的。lua脚本不被支持。
  • 由于集群方案出现较晚,很多公司已经采用了其他的集群方案,而其它方案想要迁移 至redis cluster,需要整体迁移而不是逐步过渡,复杂度较大。

Comment