1. NoSQL的引言

NoSQL(Not Only SQL ),意即不仅仅是SQL, 泛指非关系型的数据库。Nosql这个技术门类早期就有人提出,发展至2009年趋势越发高涨。

关系型数据库(RDBMS):通过SQL语句操作 存储数据 如:MySQL、Oracle等

非关系型数据库:不一定通过SQL语句操作 存储数据

2. 为什么是NoSQL?

随着互联网网站的兴起,传统的关系数据库在应付动态网站,特别是超大规模和高并发的纯动态网站已经显得力不从心,暴露了很多难以克服的问题。如商城网站中对商品数据频繁查询(缓存)、对热搜商品的排行统计(统计)、订单超时问题(过期自动超时)、以及微信朋友圈(音频,视频)存储((存储)等相关使用传统的关系型数据库实现就显得非常复杂,虽然能实现相应功能但是在性能上却不是那么乐观。nosql这个技术门类的出现,更好的解决了这些问题,它告诉了世界不仅仅是sql,提出了对问题的新的解决方案。

3. NoSQL的四大分类

3.1 键值对(Key-Value)存储数据库

image-20240404105739497

3.2 列存储数据库

image-20240404105757482

3.3 文档型(Document)数据库

image-20240404105813636

3.4 图形(Graph)数据库

image-20240404105844769

4. NoSQL应用场景

  • 数据模型比较简单
  • 需要灵活性更强的IT系统(系统设计灵活、性能要求比较高)
  • 对数据库性能要求较高
  • 不需要高度的数据一致性(NoSQL产品对事务支持不是特别良好)

5. 什么是Redis?

官网:Redis

中文网站:CRUG网站 ,软件更新速度较慢

image-20240404105923265

Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache and message broker.

新版:

image-20240404105946780

Redis:开源 遵循BSD 基于内存数据存储 被用于作为数据库、缓存、消息中间件

redis数据在内存中 内存:读写快 断电立即消失

机制:持久化机制 定期将内存中数据写入到磁盘(ROM,数据持久化)中,保证数据不丢失

数据库:硬盘存储数据 —通过io读到内存—操作

总结:redis是一个内存型的数据库 缓存 消息中间件等

6. Redis特点

  • Redis是一个高性能key/value内存型数据库

  • Redis支持丰富的数据类型(String、List、Set、Zset、Hash),相比于Memcache(字符串,对象) 丰富

  • Redis支持持久化,可将内存中数据持久化到磁盘中

  • Redis单进程、单线程,借鉴了Memcache(多线程、线程锁),效率高,不支持并发,不存在线程安全问题(所有请求都是一个一个走):Redis实现分布式锁

7. Redis安装

image-20240404110426973

image-20240404110506900

image-20240404110540715

8. Redis数据库相关指令

8.0redis细节

image-20240404110640503

8.1 数据库操作指令

1
2
3
4
5
6
7
8
9
10
# 1.Redis中库说明
- 使用redis的默认配置器动redis服务后,默认会存在16个库,编号从0-15
- 可以使用select 库的编号 来选择一个redis的库

# 2.Redis中操作库的指令
- 清空当前的库 FLUSHDB(Tab键提示且自动变大写)
- 清空全部的库 FLUSHALL

# 3.redis客户端显示中文
- ./redis-cli -p 7000 --raw

8.2 操作key相关指令

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
101
102
# 1.DEL指令
- 语法 : DEL key [key ...]
- 作用 : 删除给定的一个或多个key 。不存在的key 会被忽略。
- 可用版本: >= 1.0.0
- 返回值: 被删除key 的数量。
- 举例:del name

# 2.EXISTS指令
- 语法: EXISTS key
- 作用: 检查给定key 是否存在。
- 可用版本: >= 1.0.0
- 返回值: 若key 存在,返回1 ,否则返回0。
- 举例:exists name,存在返回1,不存在返回0
- exists name age bir,存在几个返回几,都不存在返回0(key可指定多个)

# 3.EXPIRE指令(解决超时问题)
- 语法: EXPIRE key seconds
- 作用: 为给定key 设置生存时间,当key 过期时(生存时间为0 ),它会被自动删除。
- 可用版本: >= 1.0.0
- 时间复杂度: O(1)
- 返回值:设置成功返回1 。
- 举例:expire name 10,设置name的有效期为10秒

# 4.KEYS
- 语法 : KEYS pattern
- 作用 : 查找所有符合给定模式pattern的key 。
- 语法:
KEYS *:匹配数据库中所有key 。
KEYS h?llo:?表示匹配一个字符,匹配hello ,hallo 和hxllo等。
KEYS h*llo:*表示匹配0到任意多个字符,匹配hllo 和heeeeello 等。
KEYS h[ae]llo :[]表示匹配一个,匹配a或者e,匹配hello 和hallo ,但不匹配hillo 。特殊符号用 "\" 隔开
- 可用版本: >= 1.0.0
- 返回值: 符合给定模式的key 列表。

# 5.MOVE
- 语法 : MOVE key db
- 作用 : 将当前数据库的key 移动到给定的数据库db当中。
- 可用版本: >= 1.0.0
- 返回值: 移动成功返回1 ,失败则返回0。
- 举例:move name 1,将key对应name的键值对移到1号库

# 6.PEXPIRE
- 语法 : PEXPIRE key milliseconds
- 作用 : 这个命令和EXPIRE命令的作用类似,但是它以毫秒为单位设置key的生存时间,而不像EXPIRE命令那样,以秒为单位。
- 可用版本: >= 2.6.0
- 时间复杂度: O(1)
- 返回值:设置成功,返回1;key不存在或设置失败,返回0
- 举例:pexpire name 10

# 7.PEXPIREAT
- 语法 : PEXPIREAT key milliseconds-timestamp
- 作用 : 这个命令和EXPIREAT命令类似,但它以毫秒为单位设置key的过期unix时间戳,而不是像EXPIREAT那样,以秒为单位。
- 可用版本: >= 2.6.0
- 返回值:如果生存时间设置成功,返回1。当key不存在或没办法设置生存时间时,返回0。(查看EXPIRE命令获取更多信息)
- 举例:pexpireat name 5000,设置name的有效期为5000毫秒,即5秒

# 8.TTL
- 语法 : TTL key
- 作用 : 以秒为单位,返回给定key的剩余生存时间(TTL, time to live)。
- 可用版本: >= 1.0.0
- 返回值:
当key不存在时,返回-2 。
当key存在但没有设置剩余生存时间时(永久存储),返回-1。
否则,返回>=0的数,以秒为单位,返回key的剩余生存时间。
- Note : 在Redis 2.8以前,当key不存在,或者key没有设置剩余生存时间时,命令都返回-1。
- 举例:ttl name

# 9.PTTL
- 语法 : PTTL key
- 作用 : 这个命令类似于TTL命令,但它以毫秒为单位返回key的剩余生存时间,而不是像TTL命令那样,以秒为单位。
- 可用版本: >= 2.6.0
- 返回值: 当key 不存在时,返回-2 。当key存在但没有设置剩余生存时间时,返回-1。
- 否则,以毫秒为单位,返回key 的剩余生存时间。
- 注意 : 在Redis 2.8以前,当key不存在,或者key没有设置剩余生存时间时,命令都返回-1。
- 举例:ttl name

# 10.RANDOMKEY
- 语法 : RANDOMKEY
- 作用 : 从当前数据库中随机返回(不删除) 一个key 。
- 可用版本: >= 1.0.0
- 返回值:当数据库不为空时,返回一个key 。当数据库为空时,返回nil。
- 举例:randomkey name,随机返回一个key

# 11.RENAME
- 语法 : RENAME key newkey
- 作用 : 将key改名为newkey。当key和newkey相同,或者key不存在时,返回一个错误。当newkey已经存在时,RENAME命令将覆盖旧值。
- 可用版本: >= 1.0.0
- 返回值: 改名成功时提示OK,失败时候返回一个错误。
- 举例:rename name username

# 12.TYPE
- 语法 : TYPE key
- 作用 : 返回key所储存的值的类型。
- 可用版本: >= 1.0.0
- 返回值:
none (key 不存在)
string (字符串)
list (列表)
set (集合)
zset (有序集)
hash (哈希表)
- 举例:type name

上边讲的是<key,value> 的key

下边就要讲<key,value>的value value[String,list,zset,set,hash] value的类型可以有很多


8.3 String类型

8.3.1 内存存储模型

key:字符串 value:String字符串

image-20240404141842053

8.3.2 常用操作命令
命令 说明
set 键名 值 设置一个key/value
get 键名 根据key获得对应的value
mset(more set) 键名1 值1 键名2 值2 键名3 值3 一次设置多个key value
mget(more get) 键名1 键名2 键名3 一次获得多个key的value
getset 键名 新值 获得原始key的值,同时设置新值
strlen 键名 获得对应key存储value的长度
append 键名 值追加内容 为对应key的value追加内容
getrange(索引从0开始,最后索引可用-1代替) 键名 起点 终点 截取指定范围value的内容
setex 键名 存活时间(秒) 值;ttl 键名(查看存活时间) 设置一个key存活的有效期(秒)
psetex键名 存活时间(毫秒) 值;pttl 键名(查看存活时间) 设置一个key存活的有效期(毫秒)
setnx(set not exists)键名 值 键存在,不做任何操作;键不存在,执行添加操作
msetnx(原子操作:只要有一个存在不做任何操作,要都不存在才执行) 键名1 值1 键名2 值2 键名3 值3 可以同时设置多个key,只要有一个存在就不做任何处理
decr 键名 进行数值类型的-1操作
decrby 键名 步长 根据提供的数据进行减法操作
incr 键名 进行数值类型的+1操作
incrby 键名 步长 根据提供的数据进行加法操作
Incrbyfloat 键名 小数步长(精度保留17位) 根据提供的数据加入浮点数

8.4 List类型

list:列表,相当于java中list 集合

特点:元素有序且可以重复

8.4.1 内存存储模型

key:字符串 value:list相当于java中list 集合

image-20240404142538945

8.4.2 常用操作指令
命令 说明
lpush 列表名 值1 值2 值3(从左侧放) 将某个值加入到一个key列表头部
lpushx(lpush exists) 列表名 值1 值2 值3(从左侧放) 同lpush,但是必须要保证这个key存在(不能创建列表,列表已经被创建时才能使用)
rpush 列表名 值1 值2 值3(从右侧放) 将某个值加入到一个key列表末尾
rpushx(rpush exists) 列表名 值1 值2 值3(从右侧放) 同rpush,但是必须要保证这个key存在(不能创建列表,列表已经被创建时才能使用)
lpop 列表名 返回和移除列表左边的第一个元素
rpop 列表名 返回和移除列表右边的第一个元素
lrange 列表名 起点(从0开始) 终点(-1表示末尾) 【遍历时:0 -1】 获取某一个下标区间内的元素
llen 列表名 获取列表元素个数
lset 列表名 索引 值 设置某一个指定索引的值(索引必须存在)
lindex 列表名 索引 获取某一个指定索引位置的元素
lrem 列表名 重复元素个数 值(从左侧开始匹配,重复时删除) 删除重复元素
ltrim 列表名 索引起点 索引终点(索引从0开始) 保留列表中特定区间内的元素
linsert 列表名 before/after 值 新值 在某一个元素之前/之后插入新元素

8.5 Set类型

特点:Set类型 Set集合 元素无序 不可以重复

8.5.1 内存存储模型

key:字符串 value: set集合

一键(String)对多值

image-20240404142806270

8.5.2 常用命令
命令 说明
sadd 键名 值1 值2 值 没有集合时会先创建集合,然后为集合添加元素
smembers 键名 显示集合中所有元素,无序
scard 键名 返回集合中元素的个数
spop 键名 随机返回一个元素,并将元素在集合中删除
smove 键名1 键名2 值 从一个集合中向另一个集合移动元素,必须是同一种类型
srem 键名 值 从集合中删除一个元素
sismember 键名 值 判断一个集合中是否含有这个元素,有就返回1,没有返回0
srandmember 键名 元素个数 随机返回元素,不删除
sdiff 键名1 键名2 键名3 … 去掉第一个集合中其它集合含有的相同元素
sinter 键名1 键名2 键名3 … 求交集(去重)
sunion 键名1 键名2 键名3 … 求和集

8.6 ZSet类型

特点: 可排序的set集合 排序 不可重复

ZSET 官方 | 可排序SET sortSet

8.6.1 内存模型

image-20240404143015532

8.6.2 常用命令
命令 说明
zadd 键名 分数1 值1 分数2 值2 分数3 值3 没有集合时创建集合,然后添加集合元素,元素有序且不重复
zcard 键名 返回集合的元素个数
zrange(升序)/zrevrange(降序 ) 键名 下标起点 下标终点 (withscores 带此参数时显示分数) 返回一个范围内的元素
zrangebyscore 键名 分数起点 分数终点 withscores limit 页码 显示条数 按照分数查找一个范围内的元素
zrank 键名 值 返回排名(正序)
zrevrank 键名 值 倒序排名
zscore 键名 值 显示某一个元素的分数
zrem 键名 值1 值2 值3…….. 移除某一个或多个元素
zincrby 键名 加分值 值 给某个特定元素加分

8.7 hash类型

redis :key(string)、value(map)

特点: value是一个map结构,存在key、value,且key是无序的

即:[ 外key, [ 内key , 内value ] ] 先写外key再内key最后内value

8.7.1 内存模型

image-20240404143200682

8.7.2 常用命令
命令 命令
hset 外key 内key 内value 设置一个key/value对
hget 外key 内key 获得一个key对应的value
hgetall 外key 获得所有的key/value对
hdel 外key 内key 删除某一个key/value对
hexists 外key 内key 判断一个key是否存在
hkeys 外key 获得所有的key
hvals 外key 获得所有的value
hmset 外key 内key1 内value1 内key2 内value2…… 设置多个key/value
hmget 外key 内key1 内key2 …… 获得多个key的value
hsetnx 外key 新内key 新内value 设置一个不存在的key的值,内key不存在时添加,存在时不添加
hincrby 外key 内key 增量 为value进行加法运算,value必须为数值类型才可以
hincrbyfloat 外key 内key 增量 为value加入浮点值,value必须为数值类型才可以

9. redis桌面可视化工具

image-20240404143453873

RedisDesktopManager(自行安装,下一步):

image-20240404143514902

ok即可,进行连接redis服务器

image-20240404143536853

image-20240404143555515

双击进行查看,按照文字进行操作:

image-20240404143626897

image-20240404143640311

9.持久化机制

client redis[内存] —–> 内存数据—数据持久化–>磁盘,即:内存 to 硬盘

Redis官方提供了两种不同的持久化方法来将数据存储到硬盘里面分别是:

  • 快照(Snapshot) / RDB

  • AOF (Append Only File) 只追加日志文件

image-20240404144615015

9.1快照(Snapshot)/RDB

9.1.1 特点

​ 这种方式可以将某一时刻的所有数据都写入硬盘中,当然这也是redis默认开启的持久化方式,保存的文件是以.rdb形式结尾的文件,因此这种方式也称之为RDB方式

image-20240404144709855

9.1.2 快照生成方式

  • 客户端方式:BGSAVE 和 SAVE指令
  • 服务器配置自动触发
1
2
3
4
# 1.客户端方式之BGSAVE
- a.客户端可以使用BGSAVE命令来创建一个快照,当接收到客户端的BGSAVE命令时,redis会调用fork来创建一个子进程,然后子进程负责将快照写入磁盘中,而父进程则继续处理命令请求。

fork:当一个进程创建子进程的时候,底层的操作系统会创建该进程的一个副本,在类unix系统中创建子进程的操作会进行优化:在刚开始的时候,父子进程共享相同内存,直到父进程或子进程对内存进行了写之后,对被写入的内存的共享才会结束服务(不会阻塞Redis服务,同时还能保证快照的创建)。

image-20240404144853573

1
2
# 2.客户端方式之SAVE
- b.客户端还可以使用SAVE命令来创建一个快照,接收到SAVE命令的redis服务器在快照创建完毕之前,将不再响应任何其他的命令

image-20240404144903842

注意:SAVE命令并不常用,使用SAVE命令在快照创建完毕之前,redis处于阻塞状态,无法对外服务。

1
2
# 3.服务器配置方式之满足配置自动触发
- 如果用户在redis.conf中设置了save配置选项,redis会在save选项条件满足之后自动触发一次BGSAVE命令,如果设置多个save配置选项,当任意一个save配置选项条件满足,redis也会触发一次BGSAVE命令。

image-20240404144919164

1
2
# 4.服务器接收客户端shutdown指令
- 当redis通过shutdown指令接收到关闭服务器的请求时,会执行一个save命令,阻塞所有的客户端,不再执行客户端执行发送的任何命令,并且在save命令执行完毕之后关闭服务器。

9.1.3 配置生成快照名称和位置

1
2
3
4
5
# 1.修改生成快照名称
- dbfilename dump.rdb

# 2.修改生成位置
- dir ./

image-20240404145010235

总结:RDB持久化(记录数据)只能保存某一时刻的数据,如果在下一次保存时间还未到来前出现宕机等情况,就会导致该时刻后的数据丢失,为此提出了AOF持久化。

9.2 只追加日志文件(AOF)

9.2.1 特点

​ 这种方式可以将所有客户端执行的写命令记录到日志文件中,AOF持久化会将被执行的写命令写到AOF的文件末尾,以此来记录数据发生的变化,因此只要redis从头到尾执行一次AOF文件所包含的所有写命令,就可以恢复AOF文件的记录的数据集(可做到丢失1秒数据甚至不丢失数据,记录写命令,以日志的形式保存到aof文件中,数据状态为二进制)。
image-20240404145104856

9.2.2 开启AOF持久化

在redis的默认配置中AOF持久化机制是没有开启的,需要在配置中开启:

1
2
3
# 1.开启AOF持久化
- a.修改 appendonly yes 开启持久化(默认为no)
- b.修改 appendfilename "appendonly.aof" 指定生成文件名称(默认名称为appendonly.aof)

image-20240404145209983

9.2.3 日志同步频率修改

1
2
# 1.修改日志同步频率(默认每秒写入)
- 修改appendfsync everysec|always|no 指定

image-20240404145252695

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1.always 【谨慎使用】
- 说明: 每个redis写命令都要同步写入硬盘,严重降低redis速度
- 解释: 如果用户使用了always选项,那么每个redis写命令都会被写入硬盘,从而将发生系统崩溃时出现的数据丢失减到最少;遗憾的是,因为这种同步策略需要对硬盘进行大量的写入操作,所以redis处理命令的速度会受到硬盘性能的限制;
- 注意: 转盘式硬盘在这种频率下200~500左右个命令/s ; 固态硬盘(SSD)几百万个命令/s;
- 警告: 使用SSD用户请谨慎使用always选项,这种模式不断写入少量数据的做法有可能会引发严重的写入放大问题,导致将固态硬盘的寿命从原来的几年降低为几个月。

# 2.everysec 【推荐】
- 说明: 每秒执行一次同步显式的将多个写命令同步到磁盘
- 解释:为了兼顾数据安全和写入性能,用户可以考虑使用everysec选项,让redis每秒一次的频率对AOF文件进行同步;redis每秒同步一次AOF文件时性能和不使用任何持久化特性时的性能相差无几,而通过每秒同步一次AOF文件,redis可以保证,即使系统崩溃,用户最多丢失一秒之内产生的数据。

# 3.no 【不推荐】
- 说明: 由操作系统决定何时同步
- 解释:最后使用no选项,将完全由操作系统决定什么时候同步AOF日志文件,这个选项不会对redis性能带来影响,但是系统崩溃时,会丢失不定数量的数据,甚至全部数据;另外,如果用户硬盘处理写入操作不够快的话,当缓冲区被等待写入硬盘数据填满时(一次需要大量写入aof,磁盘处理不过来),redis会处于阻塞状态,并导致redis的处理命令请求的速度变慢。
总结:持久化机制默认优先执行AOF(数据丢失少,可通过不执行SAVE或BGSAVE来停止RDB持久化),且AOF和RDB可同时生效。

9.2.4 AOF文件的重写

  1. AOF带来的问题

    AOF的方式也同时带来了另一个问题:持久化文件会变的越来越大,例如我们调用incr test命令100次,文件中必须保存全部的100条命令,其实有99条都是多余的,因为要恢复数据库的状态其实文件中保存一条set test 100就够了。为了压缩aof的持久化文件Redis提供了AOF重写(ReWriter)机制。

  2. AOF重写

    用来在一定程度上减小AOF文件的体积

  3. 触发重写方式

    1
    2
    3
    4
    5
    6
    7
    8
    # 1.客户端方式触发重写
    - 执行BGREWRITEAOF命令 不会阻塞redis的服务

    # 2.服务器配置方式自动触发
    - 配置redis.conf中的auto-aof-rewrite-percentage选项 参加下图↓↓↓
    - 如果设置auto-aof-rewrite-percentage值为100和auto-aof-rewrite-min-size 64mb,并且启用的AOF持久化时,那么当AOF文件体积大于64M,并且AOF文件的体积比上一次重写之后体积大了至少一倍(100%)时,会自动触发,如果重写过于频繁,用户可以考虑将auto-aof-rewrite-percentage设置为更大(100:1倍、200:2倍......)。

    64MB---》20MB---》40MB---》18MB---》36MB---》26MB---》52MB

    image-20240404145507863

  4. 重写原理(文件的替换)

    注意:重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,替换原有的文件这点和快照有点类似。

    1
    2
    3
    4
    5
    # 重写流程
    - 1. redis调用fork,现在有父子两个进程,子进程根据内存中的数据状态写入数据库快照,往临时文件中写入重建数据库状态的命令。
    - 2. 父进程继续处理client请求,除了把写命令写入到原来的aof文件中。同时把收到的写命令缓存起来,这样就能保证如果子进程重写失败的话并不会出问题。
    - 3. 当子进程把快照内容写入已命令方式写到临时文件中后,子进程发信号通知父进程,然后父进程把缓存的写命令也写入到临时文件。
    - 4. 现在父进程可以使用临时文件替换老的aof文件,并重命名,后面收到的写命令也开始往新的aof文件中追加。

    image-20240404145609748

9.3 持久化总结

​ 两种持久化方案既可以同时使用(aof),又可以单独使用,在某种情况下也可以都不使用,具体使用那种持久化方案取决于用户的数据和应用决定。

​ 无论使用AOF还是快照机制持久化,将数据持久化到硬盘都是有必要的,除了持久化之外,用户还应该对持久化的文件备份(最好备份在多个不同地方)

10.java操作redis

9.1 环境准备

新建普通的java maven项目,archetype选择quickstart,main下新建resources目录。

9.1.1 引入依赖

1
2
3
4
5
6
<!--引入redis连接依赖jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>

9.1.2 创建jedis对象测试redis连接及库相关操作ApI333

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
package com.study.test;

import redis.clients.jedis.Jedis;

import java.util.Set;

/**
* @ClassName TestRedis
* @Description 测试Redis连接
* @Author chabai
* @Date 2022/9/11 13:31
* @Version 1.0
*/
public class TestRedis {
public static void main(String[] args) {
// 1、创建jedis客户端对象
Jedis jedis = new Jedis();// 不指定时使用本机地址,端口号默认为6379
//Jedis jedis = new Jedis("192.168.1.7",6379);//redis服务必须关闭防火墙 redis服务必须开启远程连接

// 选择库,默认使用0号库
jedis.select(0);

// 获取redis中所有key信息
Set<String> keys = jedis.keys("*");
keys.forEach(key-> System.out.println("key = " + key));

// 2、操作相关
// 1.清空当前库
jedis.flushDB();
// 2.清空所有库
jedis.flushAll();

// 3、释放资源
jedis.close();
}
}

注意:

  • redis服务必须关闭防火墙
  • redis服务必须开启远程连接

未启动redis-server.exe(开启远程连接)时报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Exception in thread "main" redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: connect timed out
at redis.clients.jedis.Connection.connect(Connection.java:207)
at redis.clients.jedis.BinaryClient.connect(BinaryClient.java:93)
at redis.clients.jedis.Connection.sendCommand(Connection.java:126)
at redis.clients.jedis.BinaryClient.select(BinaryClient.java:176)
at redis.clients.jedis.BinaryJedis.select(BinaryJedis.java:522)
at com.study.test.TestRedis.main(TestRedis.java:21)
Caused by: java.net.SocketTimeoutException: connect timed out
at java.net.DualStackPlainSocketImpl.waitForConnect(Native Method)
at java.net.DualStackPlainSocketImpl.socketConnect(DualStackPlainSocketImpl.java:81)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:476)
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:218)
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:200)
at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:162)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:394)
at java.net.Socket.connect(Socket.java:606)
at redis.clients.jedis.Connection.connect(Connection.java:184)
... 5 more

正常启动后,idea控制台输出0号库的所有key信息。

10.2 操作key相关API

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
package com.study.test;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import redis.clients.jedis.Jedis;

/**
* @ClassName TestKey
* @Description TODO
* @Author chabai
* @Date 2022/9/11 13:52
* @Version 1.0
*/
public class TestKey {
private Jedis jedis;

@Before
public void before(){
this.jedis = new Jedis("192.168.1.7",6379);
}

@After
public void after(){
jedis.close();
}

/**
* 测试key相关
*/
@Test
public void testKeys(){
// 删除一个key
jedis.del("name");
// 删除多个key
//jedis.del("name","age");

// 判断一个key是否存在
Boolean name = jedis.exists("name");
System.out.println("name = " + name);

//设置key的超时时间
Long bir = jedis.expire("bir", 100);
System.out.println("bir = " + bir);// 返回1表示未超时

// 查看超时时间
Long bir1 = jedis.ttl("bir");
System.out.println("bir1 = " + bir1);

// 获取一个随机key
String s = jedis.randomKey();
System.out.println("s = " + s);

// 修改key名称
String rename = jedis.rename("age", "newAge");
System.out.println("rename = " + rename);

// 查看key对应值的类型
String newAge = jedis.type("newAge");
System.out.println("newAge = " + newAge);

// ...
}
}

10.3 操作String相关API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//测试String相关
@Test
public void testString(){
//set
jedis.set("name","小陈");
//get
String s = jedis.get("name");
System.out.println(s);
//mset
jedis.mset("content","好人","address","海淀区");
//mget
List<String> mget = jedis.mget("name", "content", "address");
mget.forEach(v-> System.out.println("v = " + v));
//getset
String set = jedis.getSet("name", "小明");
System.out.println(set);

//............
}

10.4 操作List相关API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
public void testList(){

//lpush
jedis.lpush("names1","张三","王五","赵柳","win7");

//rpush
jedis.rpush("names1","xiaomingming");

//lrange

List<String> names1 = jedis.lrange("names1", 0, -1);
names1.forEach(name-> System.out.println("name = " + name));

//lpop rpop
String names11 = jedis.lpop("names1");
System.out.println(names11);

//llen
jedis.linsert("lists", BinaryClient.LIST_POSITION.BEFORE,"xiaohei","xiaobai");

//........
}

10.5 操作Set的相关API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void testSet(){

//sadd
jedis.sadd("names","zhangsan","lisi");

//smembers
jedis.smembers("names");

//sismember
jedis.sismember("names","xiaochen");

//...
}

10.6 操作ZSet相关API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void testZset(){

//zadd
jedis.zadd("names",10,"张三");

//zrange
jedis.zrange("names",0,-1);

//zcard
jedis.zcard("names");

//zrangeByScore
jedis.zrangeByScore("names","0","100",0,5);

//..
}

10.7 操作Hash相关API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void testHash(){
//hset
jedis.hset("maps","name","zhangsan");
//hget
jedis.hget("maps","name");
//hgetall
jedis.hgetAll("mps");
//hkeys
jedis.hkeys("maps");
//hvals
jedis.hvals("maps");
//....
}

10.8 项目目录结构

image-20240404150224264

11.springboot整合redis

​ Spring Boot Data(Spring Data,操作数据框架) Redis中提供了RedisTemplate和StringRedisTemplate,其中,StringRedisTemplate是RedisTemplate的子类,两个方法基本一致,不同之处主要体现在操作的数据类型不同,RedisTemplate中的两个泛型都是Object,意味着存储的key和value都可以是一个对象,而StringRedisTemplate的两个泛型都是String,意味着StringRedisTemplate的key和value都只能是字符串。

RedisTemplate (Object,Object) : 自动序列化 自动反序列化

StringRedisTemplate (String,String): 自动序列化 自动反序列化

注意: 使用RedisTemplate默认是将对象序列化到Redis中,所以放入的对象必须实现对象序列化接口

11.1环境准备

​ 新建Spring Initializr项目redis-spring-boot,引入spring-boot-devtools、lombok、spring-boot-starter-test、spring-boot-starter-web对应依赖。

11.1.1 pom.xml中引入依赖

1
2
3
4
5
<!--引入spring-boot-data-redis依赖(内含jedis依赖)-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

刷新Maven!

11.1.2application.properties配置redis

1
2
3
4
# redis配置
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.database=0

11.2使用StringRedisTemplate和RedisTemplate

11.2.1 测试StringRedisTemplate
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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
package com.study.test;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.DefaultTypedTuple;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;

import java.util.*;
import java.util.concurrent.TimeUnit;


/**
* @ClassName TestStringRedisTemplate
* @Description TODO
* @Author chabai
* @Date 2022/9/12 11:32
* @Version 1.0
*/
@SpringBootTest// 此处不需要引入@RunWith注解了
// 针对@RunWith注解找不到的问题:https://blog.csdn.net/u014641168/article/details/125405225
public class TestStringRedisTemplate {
// 注入StringRedisTemplate
@Autowired
private StringRedisTemplate stringRedisTemplate;

/**
* 操作redis中的字符串:opsForValue()
*/
@Test
public void testString(){
stringRedisTemplate.opsForValue().set("name","zhangsan");
String name = stringRedisTemplate.opsForValue().get("name");
System.out.println("name = " + name);

stringRedisTemplate.opsForValue().set("code","1234",60, TimeUnit.SECONDS);
Long code = stringRedisTemplate.getExpire("code");
System.out.println("code = " + code);

Integer append = stringRedisTemplate.opsForValue().append("code", "OneTwoThreeFour");
System.out.println("append = " + append);
}

/**
* 操作redis中key相关
*/
@Test
public void testKey(){
// 删除一个key
// Boolean name = stringRedisTemplate.delete("name");
// System.out.println("name = " + name);

// 判断key是否存在
Boolean name1 = stringRedisTemplate.hasKey("name");
System.out.println("name1 = " + name1);

// 查看所有key
Set<String> keys = stringRedisTemplate.keys("*");
keys.forEach(key-> System.out.println("key = " + key));

// 获得超时时间
Long name2 = stringRedisTemplate.getExpire("name");
System.out.println("name2 = " + name2);
}

/**
* 操作redis中list类型:opsForList()
*/
@Test
public void testList(){
// 添加元素
// stringRedisTemplate.opsForList().leftPush("names","zhangsan");
// 添加多个元素
// stringRedisTemplate.opsForList().leftPushAll("names","lisi","wangwu");
// 以集合形式进行添加元素
// List<String> names = new ArrayList<>();
// names.add("xiaoming");
// names.add("xiaohua");
// stringRedisTemplate.opsForList().leftPushAll("names",names);

// 遍历list
List<String> names1 = stringRedisTemplate.opsForList().range("names", 0, -1);
for (String s : names1) {
System.out.println("s = " + s);
}

// 截取指定区间的list,无返回值
stringRedisTemplate.opsForList().trim("names",1,3);
List<String> names = stringRedisTemplate.opsForList().range("names", 0, -1);
names.forEach(name-> System.out.println("name = " + name));
}

/**
* 操作redis中set类型:opsForSet()
*/
@Test
public void testSet(){
// 添加元素
Long add = stringRedisTemplate.opsForSet().add("sets", "zhangsan", "zhangsan", "lisi", "wangwu");
System.out.println("add = " + add);
// 遍历元素
for (String set : stringRedisTemplate.opsForSet().members("sets")) {
System.out.println("set = " + set);
}

// 获得元素个数
Long sets = stringRedisTemplate.opsForSet().size("sets");
System.out.println("sets = " + sets);
}

/**
* 操作redis中Zset类型: opsForZSet()
*/
@Test
public void testZset(){
// 添加单个元素
stringRedisTemplate.opsForZSet().add("zsets","小明",80);

//以集合形式添加元素
Set<ZSetOperations.TypedTuple<String>> tuples = new HashSet<>();
for (int i = 0; i < 5; i++) {
DefaultTypedTuple<String> tuple = new DefaultTypedTuple<>("张三" + i, 1D + i);
tuples.add(tuple);
}
stringRedisTemplate.opsForZSet().add("zsets",tuples);

// 遍历value
Set<String> zsets = stringRedisTemplate.opsForZSet().range("zsets", 0, -1);
zsets.forEach(value-> System.out.println("value = " + value));

System.out.println("---");

// 遍历value-score
Set<ZSetOperations.TypedTuple<String>> zsets1 = stringRedisTemplate.opsForZSet().rangeByScoreWithScores("zsets", 0, 100);
for (ZSetOperations.TypedTuple<String> stringTypedTuple : zsets1) {
System.out.println("stringTypedTuple = " + stringTypedTuple);
System.out.println(stringTypedTuple.getValue());
System.out.println(stringTypedTuple.getScore());
}
}

/**
* 操作redis中Hash类型:
*/
@Test
public void testHash(){
// 存入一个元素
stringRedisTemplate.opsForHash().put("hashs","name","张三");

// 以集合形式存入元素
Map<String,String> map = new HashMap<>();
map.put("age","20");
map.put("bir","2012-12-12");
stringRedisTemplate.opsForHash().putAll("hashs",map);

// 获取一个值
String val = (String) stringRedisTemplate.opsForHash().get("hashs", "name");
System.out.println("val = " + val);

// 获取多个值
List<Object> objects = stringRedisTemplate.opsForHash().multiGet("hashs", Arrays.asList("name", "age"));
objects.forEach(vals-> System.out.println("vals = " + vals));

// 获取所有值
List<Object> hashs = stringRedisTemplate.opsForHash().values("hashs");
hashs.forEach(hash-> System.out.println("hash = " + hash));

// 获取所有key
Set<Object> hashs1 = stringRedisTemplate.opsForHash().keys("hashs");
hashs1.forEach(hash-> System.out.println("hash = " + hash));
}

}
11.2.2 测试RedisTemplate

新建对象User

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
package com.study.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

import java.io.Serializable;
import java.util.Date;

/**
* @ClassName User
* @Description TODO
* @Author chabai
* @Date 2022/9/12 15:37
* @Version 1.0
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class User implements Serializable {
private String id;
private String name;
private Integer age;
private Date bir;
}

测试RedisTemplate:key、value均会被序列化

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
package com.study.test;

import com.study.entity.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.util.Date;
import java.util.UUID;

/**
* @ClassName TestRedisTemplate
* @Description TODO
* @Author chabai
* @Date 2022/9/12 15:38
* @Version 1.0
*/
@SpringBootTest
public class TestRedisTemplate {
// 注入RedisTemplate<Object,Object>
@Autowired
private RedisTemplate redisTemplate;

/**
* 测试RedisTemplate
*/
@Test
public void testRedisTemplate(){
// 获取redis序列化
RedisSerializer defaultSerializer = redisTemplate.getDefaultSerializer();
System.out.println("defaultSerializer = " + defaultSerializer);
//defaultSerializer = org.springframework.data.redis.serializer.JdkSerializationRedisSerializer@4c731956

/**
* redisTemplate对象中 key 和 value 的序列化都是 JdkSerializationRedisSerializer
* key: string
* value: object
* 修改默认key序列化方案 : key StringRedisSerializer
*/
// 修改key采用string序列化方式
redisTemplate.setKeySerializer(new StringRedisSerializer());
// 修改hash key采用string序列化方式
redisTemplate.setHashKeySerializer(new StringRedisSerializer());


// User必须序列化
User user = new User()
.setId(UUID.randomUUID().toString())
.setName("小崔")
.setAge(18)
.setBir(new Date());
redisTemplate.opsForValue().set("user",user);
Object user1 = redisTemplate.opsForValue().get("user");
System.out.println("user1 = " + user1);

redisTemplate.opsForList().leftPush("list",user);

redisTemplate.opsForSet().add("set",user);

redisTemplate.opsForZSet().add("zset",user,100);

// hash有两个key,默认均为jdk序列化方式
redisTemplate.opsForHash().put("hash","name",user);
}
}

key采用jdk序列化:

image-20240404150718697

key采用string序列化:

image-20240404150734799

添加后获取key结果:

image-20240404150749392

11.2.3 Spring Data提供的Bound API

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
package com.study.test;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.*;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import javax.security.auth.callback.NameCallback;
import java.util.List;

/**
* @ClassName TestBoundAPI
* @Description TODO
* @Author chabai
* @Date 2022/9/12 16:42
* @Version 1.0
*/
@SpringBootTest
public class TestBoundAPI {
@Autowired
private RedisTemplate redisTemplate;

@Autowired
private StringRedisTemplate stringRedisTemplate;

/**
* Spring Data为了方便我们对redis进行更加友好的操作,提供了Bound API简化操作
* 1.针对于日后处理key value 都是 String 使用 StringRedisTemplate
* 2.针对于日后处理的key value 存在对象 使用 RedisTemplate
* 3.针对于同一个key多次操作可以使用boundXXxOps() Value List Set Zset Hash的api 简化书写
*/
@Test
public void testBound(){
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());

// 通常都是将一个key进行多次绑定后操作,操作比较麻烦
stringRedisTemplate.opsForValue().set("name","小崔");
stringRedisTemplate.opsForValue().append("name","加油!");
String name = stringRedisTemplate.opsForValue().get("name");
System.out.println("name = " + name);

// 对string类型key进行绑定,后续所有操作都是基于这个key的操作
BoundValueOperations<String, String> nameValueOperation = stringRedisTemplate.boundValueOps("name");
nameValueOperation.set("小猪猪");
nameValueOperation.append("棒棒哒!");
String s = nameValueOperation.get();
System.out.println("s = " + s);

// 对list类型key进行绑定
BoundListOperations<String, String> boundListOperations = stringRedisTemplate.boundListOps("list");
boundListOperations.leftPushAll("熊大","熊二","光头强");
List<String> lists = boundListOperations.range(0, -1);
lists.forEach(list -> System.out.println("list = " + list));

// 对set类型key进行绑定
BoundZSetOperations<String, String> set = stringRedisTemplate.boundZSetOps("set");

// 对zset类型进行绑定
BoundZSetOperations<String, String> stringBoundZSetOperations = stringRedisTemplate.boundZSetOps("zset");

// 对hash类型key进行绑定
BoundHashOperations<String, Object, Object> boundHashOperations = stringRedisTemplate.boundHashOps("hash");
}
}

附视频中测试代码:

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
@Autowired
private StringRedisTemplate stringRedisTemplate; //对字符串支持比较友好,不能存储对象

@Autowired
private RedisTemplate redisTemplate; //存储对象

@Test
public void testRedisTemplate(){
System.out.println(redisTemplate);
//设置redistemplate值使用对象序列化策略
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<Object>(Object.class));//指定值使用对象序列化
//redisTemplate.opsForValue().set("user",new User("21","小黑",23,new Date()));
User user = (User) redisTemplate.opsForValue().get("user");
System.out.println(user);
// Set keys = redisTemplate.keys("*");
// keys.forEach(key -> System.out.println(key));
/*Object name = redisTemplate.opsForValue().get("name");
System.out.println(name);*/

//Object xiaohei = redisTemplate.opsForValue().get("xiaohei");
//System.out.println(xiaohei);
/*redisTemplate.opsForValue().set("name","xxxx");
Object name = redisTemplate.opsForValue().get("name");
System.out.println(name);*/
/*redisTemplate.opsForList().leftPushAll("lists","xxxx","1111");
List lists = redisTemplate.opsForList().range("lists", 0, -1);
lists.forEach(list-> System.out.println(list));*/
}


//key的绑定操作 如果日后对某一个key的操作及其频繁,可以将这个key绑定到对应redistemplate中,日后基于绑定操作都是操作这个key
//boundValueOps 用来对String值绑定key
//boundListOps 用来对List值绑定key
//boundSetOps 用来对Set值绑定key
//boundZsetOps 用来对Zset值绑定key
//boundHashOps 用来对Hash值绑定key

@Test
public void testBoundKey(){
BoundValueOperations<String, String> nameValueOperations = stringRedisTemplate.boundValueOps("name");
nameValueOperations.set("1");
//yuew
nameValueOperations.set("2");
String s = nameValueOperations.get();
System.out.println(s);
}

//hash相关操作 opsForHash
@Test
public void testHash(){
stringRedisTemplate.opsForHash().put("maps","name","小黑");
Object o = stringRedisTemplate.opsForHash().get("maps", "name");
System.out.println(o);
}

//zset相关操作 opsForZSet
@Test
public void testZSet(){
stringRedisTemplate.opsForZSet().add("zsets","小黑",10);
Set<String> zsets = stringRedisTemplate.opsForZSet().range("zsets", 0, -1);
zsets.forEach(value-> System.out.println(value));
}

//set相关操作 opsForSet
@Test
public void testSet(){
stringRedisTemplate.opsForSet().add("sets","xiaosan","xiaosi","xiaowu");
Set<String> sets = stringRedisTemplate.opsForSet().members("sets");
sets.forEach(value-> System.out.println(value));
}

//list相关的操作opsForList
@Test
public void testList(){
// stringRedisTemplate.opsForList().leftPushAll("lists","张三","李四","王五");
List<String> lists = stringRedisTemplate.opsForList().range("lists", 0, -1);
lists.forEach(key -> System.out.println(key));
}

//String相关的操作 opsForValue
@Test
public void testString(){
//stringRedisTemplate.opsForValue().set("166","好同学");
String s = stringRedisTemplate.opsForValue().get("166");
System.out.println(s);
Long size = stringRedisTemplate.opsForValue().size("166");
System.out.println(size);
}

//key相关的操作
@Test
public void test(){
Set<String> keys = stringRedisTemplate.keys("*");//查看所有key
Boolean name = stringRedisTemplate.hasKey("name");//判断某个key是否存在
stringRedisTemplate.delete("age");//根据指定key删除
stringRedisTemplate.rename("","");//修改key的名称
stringRedisTemplate.expire("key",10, TimeUnit.HOURS);
//设置key超时时间 参数1:设置key名 参数2:时间 参数3:时间的单位
stringRedisTemplate.move("",1);//移动key
}

11.3 项目最终目录结构

image-20240404150902160

12.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
1.利用redis中字符串类型完成:项目中手机验证码存储的实现(有效时间)

2.利用redis中字符串类型完成:具有失效性业务功能,如:12306、淘宝等,离订单支付关闭还有:40分钟

3.利用redis实现分布式集群系统中Session共享问题
memcache:内存、数据存储上限、数据类型比较简单
redis:内存、解决数据上限、数据类型丰富

4.利用redis的zset类型(可排序set类型):元素 分数
实现如:排行榜之类的功能
dangdang 销量排行:sales(zset) [商品id,商品销量] ......
唯一标识 分数 可以取前多少区间内的数据,分页显示等

5.利用redis实现分布式缓存(提高查询效率)

6.利用redis存储认证之后token信息(超时特性)
如:微信小程序、微信公众号
用户 openid(用户唯一标识)---> 令牌(token,解决安全问题) redis设置超时时间,时间过后失效

7.利用redis解决分布式集群系统中分布式锁问题
jvm 1进程开启多个线程 synchronize int n=20
jvm 1进程开启多个线程 synchronize int n=20 集群
..... LRA脚本
redis 单进程 单线程 n 20 定义
incr -1
decr 1

13.mybatis自身本地缓存实现

image-20240404151107063

image-20240404151119450

补充:

  • 如果数据更新极其频繁,这种情况下不建议使用缓存,因为这种情况下直接从数据库中查询更新后的结果返回即可,一旦将这种数据放入缓存,在更新数据库的同时,还要把缓存中的数据清空或者更新数据,极大地影响了更新业务的属性。一定是将极少发生修改或变动的数据存入缓存。

  • MySQL集群、Tomcat集群、Memcached集群

  • 分布式建立在集群之上

1.mybatis自身本地缓存实现

mybatis提供了一级、二级缓存,其中:

一级缓存:线程级别的缓存,也称为本地缓存或sqlSession级别的缓存,一级缓存是默认开启的。当处于同一个会话时,相同两次查询的操作就会从缓存中取。
二级缓存:全局范围的缓存,除了当前sqlSession能用外,其他的也可以使用。二级缓存默认也是开启的,只需要在mapper文件中写一个标签即可实现,且在实现时要求pojo必须实现序列化的接口,否则会报错。

image-20240404151320419

创建项目

新建Spring Initializr项目redis-spring-boot,引入spring-boot-devtools、lombok、spring-boot-starter-test、spring-boot-starter-web对应插件。

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
<!--引入spring-boot-data-redis依赖(内含jedis依赖)-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!--引入mybatis依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>

<!--引入mysql依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>

<!--引入数据源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.11</version>
</dependency>

刷新Maven!

application.properties配置redis、mybatis、日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# redis配置
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.database=0


# mybatis配置
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/redis?characterEncoding=UTF-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root

mybatis.mapper-locations=classpath:com/study/mapper/*.xml
mybatis.type-aliases-package=com.study.entity


# 日志配置
logging.level.com.study.dao=debug

启动类添加@MapperScan扫描注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.study;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.study.dao")
public class RedisSpringBootApplication {

public static void main(String[] args) {
SpringApplication.run(RedisSpringBootApplication.class, args);
}

}

新建entity实体类—User

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
package com.study.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

import java.io.Serializable;
import java.util.Date;

/**
* @ClassName User
* @Description TODO
* @Author chabai
* @Date 2022/9/12 15:37
* @Version 1.0
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class User implements Serializable {// mybatis要实现缓存,实体类必须序列化
private String id;
private String name;
private Integer age;
private Character sex;
private Date bir;
private String address;
}

新建dao接口—UserDao

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.study.dao;

import com.study.entity.User;

import java.util.List;

/**
* @ClassName UserDao
* @Description TODO
* @Author chabai
* @Date 2022/10/5 18:58
* @Version 1.0
*/
public interface UserDao {
List<User> findAll();
}

新建mapper文件—UserDaoMapper

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.study.dao.UserDao">
<!--使用二级缓存,默认使用的是PerpetualCache这个类,该类实现了Cache接口,此处可以省略对type="org.apache.ibatis.cache.impl.PerpetualCache"这个属性的指定-->
<cache/>

<!--findAll-->
<select id="findAll" resultType="User">
select id,name,age,sex,bir,address from t_user
</select>
</mapper>

新建service接口—IUserService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.study.service;

import com.study.entity.User;

import java.util.List;

/**
* @ClassName IUserService
* @Description TODO
* @Author chabai
* @Date 2022/10/5 19:03
* @Version 1.0
*/
public interface IUserService {
List<User> findAll();
}

编写service实现类—UserServiceImpl

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
package com.study.service;

import com.study.dao.UserDao;
import com.study.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

/**
* @ClassName UserServiceImpl
* @Description TODO
* @Author chabai
* @Date 2022/10/5 19:04
* @Version 1.0
*/
@Service
@Transactional
public class UserServiceImpl implements IUserService{
@Autowired
private UserDao userDao;

@Override
@Transactional(propagation = Propagation.SUPPORTS)
public List<User> findAll() {
return userDao.findAll();
}
}

编写测试类

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
package com.study.test;

import com.study.service.IUserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

/**
* @ClassName TestUserService
* @Description TODO
* @Author chabai
* @Date 2022/10/5 19:05
* @Version 1.0
*/
@SpringBootTest
public class TestUserService {
@Autowired
private IUserService userService;

@Test
public void test(){
userService.findAll().forEach(e-> System.out.println(e));
System.out.println("开启缓存后再次查询");
userService.findAll().forEach(e-> System.out.println(e));
}
}

(1)未开启缓存时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2022-10-06 10:56:23.410 DEBUG 31540 --- [           main] com.study.dao.UserDao.findAll            : ==>  Preparing: select id,name,age,sex,bir,address from t_user
2022-10-06 10:56:23.421 DEBUG 31540 --- [ main] com.study.dao.UserDao.findAll : ==> Parameters:
2022-10-06 10:56:23.436 DEBUG 31540 --- [ main] com.study.dao.UserDao.findAll : <== Total: 6
User(id=3, name=喜洋洋, age=30, bir=Sat Aug 13 21:17:25 CST 2022, address=羊村)
User(id=4, name=传智播客, age=40, bir=Sun Mar 04 12:04:06 CST 2018, address=北京金燕龙)
User(id=5, name=老王, age=50, bir=Wed Mar 07 17:37:26 CST 2018, address=北京)
User(id=6, name=小马宝莉, age=60, bir=Thu Mar 08 11:44:00 CST 2018, address=北京修正)
User(id=8, name=大猪猪, age=70, bir=Sat Aug 13 20:58:16 CST 2022, address=西虹市)
User(id=9, name=皮卡丘, age=80, bir=Sat Aug 13 21:03:49 CST 2022, address=黑龙江)
开启缓存后再次查询
2022-10-06 10:56:23.440 DEBUG 31540 --- [ main] com.study.dao.UserDao.findAll : ==> Preparing: select id,name,age,sex,bir,address from t_user
2022-10-06 10:56:23.440 DEBUG 31540 --- [ main] com.study.dao.UserDao.findAll : ==> Parameters:
2022-10-06 10:56:23.442 DEBUG 31540 --- [ main] com.study.dao.UserDao.findAll : <== Total: 6
User(id=3, name=喜洋洋, age=30, bir=Sat Aug 13 21:17:25 CST 2022, address=羊村)
User(id=4, name=传智播客, age=40, bir=Sun Mar 04 12:04:06 CST 2018, address=北京金燕龙)
User(id=5, name=老王, age=50, bir=Wed Mar 07 17:37:26 CST 2018, address=北京)
User(id=6, name=小马宝莉, age=60, bir=Thu Mar 08 11:44:00 CST 2018, address=北京修正)
User(id=8, name=大猪猪, age=70, bir=Sat Aug 13 20:58:16 CST 2022, address=西虹市)
User(id=9, name=皮卡丘, age=80, bir=Sat Aug 13 21:03:49 CST 2022, address=黑龙江)

结果分析:为什么默认开启了一级缓存,但是还是在两次执行相同查询时均访问了数据库?因为我们这里使用的是service,而每一次service的调用都会重新创建一个新的数据库会话,即不在同一会话中,当service方法调用结束后就会自动的提交事务、关闭会话,所以这种情况一级缓存是不生效的,只能使用二级缓存来减轻数据库访问的压力。
(2)开启缓存后测试结果:

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
2022-10-05 19:29:10.186  INFO 7252 --- [           main] com.study.test.TestUserService           : Started TestUserService in 2.038 seconds (JVM running for 2.623)
2022-10-05 19:29:10.456 INFO 7252 --- [ main] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} inited
2022-10-05 19:29:10.624 DEBUG 7252 --- [ main] com.study.dao.UserDao : Cache Hit Ratio [com.study.dao.UserDao]: 0.0
2022-10-05 19:29:10.631 DEBUG 7252 --- [ main] com.study.dao.UserDao.findAll : ==> Preparing: select id,name,age,sex,bir,address from t_user
2022-10-05 19:29:10.646 DEBUG 7252 --- [ main] com.study.dao.UserDao.findAll : ==> Parameters:
2022-10-05 19:29:10.657 DEBUG 7252 --- [ main] com.study.dao.UserDao.findAll : <== Total: 8
User(id=1, name=老王, age=10, bir=Tue Feb 27 17:47:08 CST 2018, address=北京)
User(id=2, name=小二王, age=20, bir=Fri Mar 02 15:09:37 CST 2018, address=北京金燕龙)
User(id=3, name=喜洋洋, age=30, bir=Sat Aug 13 21:17:25 CST 2022, address=羊村)
User(id=4, name=传智播客, age=40, bir=Sun Mar 04 12:04:06 CST 2018, address=北京金燕龙)
User(id=5, name=老王, age=50, bir=Wed Mar 07 17:37:26 CST 2018, address=北京)
User(id=6, name=小马宝莉, age=60, bir=Thu Mar 08 11:44:00 CST 2018, address=北京修正)
User(id=8, name=大猪猪, age=70, bir=Sat Aug 13 20:58:16 CST 2022, address=西虹市)
User(id=9, name=皮卡丘, age=80, bir=Sat Aug 13 21:03:49 CST 2022, address=黑龙江)
开启缓存后再次查询
2022-10-05 19:29:10.664 WARN 7252 --- [ main] o.apache.ibatis.io.SerialFilterChecker : As you are using functionality that deserializes object streams, it is recommended to define the JEP-290 serial filter. Please refer to https://docs.oracle.com/pls/topic/lookup?ctx=javase15&id=GUID-8296D8E8-2B93-4B9A-856E-0A65AF9B8C66
2022-10-05 19:29:10.667 DEBUG 7252 --- [ main] com.study.dao.UserDao : Cache Hit Ratio [com.study.dao.UserDao]: 0.5
User(id=1, name=老王, age=10, bir=Tue Feb 27 17:47:08 CST 2018, address=北京)
User(id=2, name=小二王, age=20, bir=Fri Mar 02 15:09:37 CST 2018, address=北京金燕龙)
User(id=3, name=喜洋洋, age=30, bir=Sat Aug 13 21:17:25 CST 2022, address=羊村)
User(id=4, name=传智播客, age=40, bir=Sun Mar 04 12:04:06 CST 2018, address=北京金燕龙)
User(id=5, name=老王, age=50, bir=Wed Mar 07 17:37:26 CST 2018, address=北京)
User(id=6, name=小马宝莉, age=60, bir=Thu Mar 08 11:44:00 CST 2018, address=北京修正)
User(id=8, name=大猪猪, age=70, bir=Sat Aug 13 20:58:16 CST 2022, address=西虹市)
User(id=9, name=皮卡丘, age=80, bir=Sat Aug 13 21:03:49 CST 2022, address=黑龙江)

结果分析:第一次查询时缓存中并没有数据,需要访问数据库;第二次查询时缓存中已经有数据了,直接从缓存中获取数据,不再通过数据库查询。

其中:Cache Hit Ratio(缓存命中率)=从内存中查询的次数 / 总查询次数,即:

  • 第一次查询时,缓存中不存在数据,需要从数据库查,所以缓存命中率即Cache Hit Ratio:0.0。
  • 第二次查询时,会直接将内存中的数据取出进行返回,所以此时的缓存命中率即Cache Hit Ratio:0.5(1/2)。
  • 以此类推,……

最终项目目录结构

image-20240404151823684

14.自定义rediscache实现分布式缓存

上面提到了实现二级缓存,需要在mapper中写上标签,该标签实际对应着mybatis提供的Cache接口,该接口有多个实现类用于提供二级缓存的实现:

image-20240404151847903

其中,< cache / >指定了mybatis中默认的Cache实现类PerpetualCache,该语句等价于:

<cache type="org.apache.ibatis.cache.impl.PerpetualCache"/>

image-20240404151921424

Cache源码如下

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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.apache.ibatis.cache;

import java.util.concurrent.locks.ReadWriteLock;

public interface Cache {
String getId();

void putObject(Object var1, Object var2);

Object getObject(Object var1);

Object removeObject(Object var1);

void clear();

int getSize();

default ReadWriteLock getReadWriteLock() {
return null;
}
}

PerpetualCache源码如下:

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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.apache.ibatis.cache.impl;

import java.util.HashMap;
import java.util.Map;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheException;

public class PerpetualCache implements Cache {
private final String id;
private final Map<Object, Object> cache = new HashMap();

public PerpetualCache(String id) {
this.id = id;
}

public String getId() {
return this.id;
}

public int getSize() {
return this.cache.size();
}

public void putObject(Object key, Object value) {
this.cache.put(key, value);
}

public Object getObject(Object key) {
return this.cache.get(key);
}

public Object removeObject(Object key) {
return this.cache.remove(key);
}

public void clear() {
this.cache.clear();
}

public boolean equals(Object o) {
if (this.getId() == null) {
throw new CacheException("Cache instances require an ID.");
} else if (this == o) {
return true;
} else if (!(o instanceof Cache)) {
return false;
} else {
Cache otherCache = (Cache)o;
return this.getId().equals(otherCache.getId());
}
}

public int hashCode() {
if (this.getId() == null) {
throw new CacheException("Cache instances require an ID.");
} else {
return this.getId().hashCode();
}
}
}

PerpetualCache现原理:将sql语句作为key,数据作为value存储在一个哈希表中,这是默认的情况,现在我们需要实现redis缓存,且这些缓存数据不再放在服务器应用上,所有我们需要自己写一个实现了Cache接口的缓存类,把该缓存的内容存储到我们的redis服务器,拿缓存也是从redis服务器拿。

14.1自定义RedisCache

注意:该类的对象必须是由mybatis创建的,mybatis会给每个mapper文件创建一个对象,每个对象的id都不一样。在配置完id的相关属性外,我们的RedisCache就能够启动了,但是并不能存取缓存数据,需要实现剩下的方法。

但是有个问题,就是我们装缓存的容器需要使用redis,所以需要操作Redis,我们可以使用上一篇文章讲到的redis整合springboot,但是RedisCache并不是IOC容器中的,是由mybatis进行维护的,不能够直接注入RedisTemplate,我们必须先自定义一个获取IOC容器的工具类,用它来获取IOC容器中的redisTemplate,详见ApplicationContextUtils.

缓存存取原理:把mybatis创建时提供的id作为我们redis中的key(外key),创建一个hashmap的数据结构,hashmap的key(内key)就是查询的sql语句,value为对应的数据。

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
package com.study.cache;

import com.study.util.ApplicationContextUtils;
import org.apache.ibatis.cache.Cache;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
* @ClassName RedisCache
* @Description 自定义RedisCache缓存实现,必须要实现Cache接口
* @Author chabai
* @Date 2022/10/5 23:14
* @Version 1.0
*/
public class RedisCache implements Cache {
// 必须指定String类型的id,即:当前放入缓存的mapper的namespace
private final String id;

// 必须提供以String类型的id作为入参的构造方法,参见PerpetualCache
public RedisCache(String id){
System.out.println("id = " + id);// id = com.study.dao.UserDao
this.id = id;
}

// 返回cache的唯一标识id,是通过构造方法传过来的
@Override
public String getId() {
return this.id;
}

/**
* @MethodName putObject
* @Description 将数据存入缓存
* @param: key
* @param: value
* @Author chabai
* @Date 23:27 2022/10/5
*/
@Override
public void putObject(Object key, Object value) {
System.out.println("key = " + key.toString());
System.out.println("value = " + value);
// // 通过application工具类获取RedisTemplate
// RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
// // 设置序列化方式
// redisTemplate.setKeySerializer(new StringRedisSerializer());
// redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// // 使用redisHash类型作为缓存存储模型
// redisTemplate.opsForHash().put(id.toString(),key.toString(),value);
getRedisTemplate().opsForHash().put(id.toString(),key.toString(),value);
}

/**
* @MethodName getObject
* @Description 从缓存中获取数据
* @param: key
* @return: java.lang.Object
* @Author chabai
* @Date 23:58 2022/10/5
*/
@Override
public Object getObject(Object key) {
System.out.println("key.toString() = " + key.toString());
// // 通过application工具类获取RedisTemplate
// RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
// // 设置序列化方式
// redisTemplate.setKeySerializer(new StringRedisSerializer());
// redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// // 根据key从redisHash类型中获取数据
// return redisTemplate.opsForHash().get(id.toString(),key.toString());
return getRedisTemplate().opsForHash().get(id.toString(),key.toString());
}

/**
* @MethodName removeObject
* @Description 移除一个缓存
* @param: key
* @return: java.lang.Object
* @Author chabai
* @Date 0:19 2022/10/6
*/
@Override
public Object removeObject(Object key) {
System.out.println("根据指定key删除缓存");
return getRedisTemplate().opsForHash().delete(id.toString(), key.toString());
}

@Override
public void clear() {// 清空所有缓存
System.out.println("清空缓存");// *****增删改时会清空Redis中的缓存*****
// // 通过application工具类获取RedisTemplate
// RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
// // 设置序列化方式
// redisTemplate.setKeySerializer(new StringRedisSerializer());
// redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// // 清空namespace
// redisTemplate.delete(id.toString());// 清空缓存
getRedisTemplate().delete(id.toString());
}

/**
* @MethodName getSize
* @Description 用来计算缓存数量/个数
* @return: int
* @Author chabai
* @Date 0:24 2022/10/6
*/
@Override
public int getSize() {
// // 通过application工具类获取RedisTemplate
// RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
// // 设置序列化方式
// redisTemplate.setKeySerializer(new StringRedisSerializer());
// redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// // 获取hash中key value数量
// return redisTemplate.opsForHash().size(id.toString()).intValue();// long to int
return getRedisTemplate().opsForHash().size(id.toString()).intValue();
}

// 封装redisTemplate
private RedisTemplate getRedisTemplate(){
RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
// 设置序列化方式
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
}

14.2自定义获取IOC容器的工具类ApplicationContextUtils

注意:该类需要实现ApplicationContextAware接口。

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
package com.study.util;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
* @ClassName ApplicationContextUtils
* @Description 获取IOC容器工具类:用来获取springboot创建好的工厂
* @Author chabai
* @Date 2022/10/5 23:35
* @Version 1.0
*/
@Component
public class ApplicationContextUtils implements ApplicationContextAware {
// 获取到IOC容器
private static ApplicationContext applicationContext;

// 获取IOC容器:将创建好的工厂以参数形式传递给这个类
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}

// 获取ioc容器
public static ApplicationContext getContext(){
return applicationContext;
}

// 直接获取通过对象名获取bean对象:提供通过对象名在工厂中获取对象的方法
public static Object getBean(String beanName){
return applicationContext.getBean(beanName);
}
}

至此,就可以在RedisCache中获取到IOC容器里面的对象了。

14.3完善IUserService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.study.service;

import com.study.entity.User;

import java.util.List;

/**
* @ClassName IUserService
* @Description TODO
* @Author chabai
* @Date 2022/10/5 19:03
* @Version 1.0
*/
public interface IUserService {
List<User> findAll();

User findById(String id);

void deleteById(String id);
}

14.4完善UserServiceImpl

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
package com.study.service;

import com.study.dao.UserDao;
import com.study.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

/**
* @ClassName UserServiceImpl
* @Description TODO
* @Author chabai
* @Date 2022/10/5 19:04
* @Version 1.0
*/
@Service
@Transactional
public class UserServiceImpl implements IUserService{
@Autowired
private UserDao userDao;

@Override
@Transactional(propagation = Propagation.SUPPORTS)
public List<User> findAll() {
return userDao.findAll();
}

@Override
@Transactional(propagation = Propagation.SUPPORTS)
public User findById(String id) {
return userDao.findById(id);
}

@Override
public void deleteById(String id) {
userDao.deleteById(id);
}
}

14.5完善UserDao

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.study.dao;

import com.study.entity.User;

import java.util.List;

/**
* @ClassName UserDao
* @Description TODO
* @Author chabai
* @Date 2022/10/5 18:58
* @Version 1.0
*/
public interface UserDao {
List<User> findAll();

User findById(String id);

void deleteById(String id);
}

14.6完善UserDaoMapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.study.dao.UserDao">
<!--使用二级缓存:使用自定义的RedisCache实现分布式缓存,之后该mapper中的二级缓存都会在redis中存或取-->
<cache type="com.study.cache.RedisCache"/>

<!--findAll-->
<select id="findAll" resultType="User">
select id,name,age,sex,bir,address from t_user
</select>

<!--findById-->
<select id="findById" parameterType="string" resultType="User">
select id,name,age,sex,bir,address from t_user where id=#{id}
</select>

<!--deleteById-->
<delete id="deleteById" parameterType="string">
delete from t_user where id=#{id}
</delete>
</mapper>

14.7完善测试类完善测试类

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
package com.study.test;

import com.study.entity.User;
import com.study.service.IUserService;
import org.apache.ibatis.cache.Cache;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

/**
* @ClassName TestUserService
* @Description TODO
* @Author chabai
* @Date 2022/10/5 19:05
* @Version 1.0
*/
@SpringBootTest
public class TestUserService {
@Autowired
private IUserService userService;

@Test
public void testFindAll(){
userService.findAll().forEach(e-> System.out.println(e));
System.out.println("开启缓存后再次查询");
userService.findAll().forEach(e-> System.out.println(e));
Cache cache;
}

@Test
public void testFindById(){
User user = userService.findById("2");
System.out.println("user = " + user);
System.out.println("开启缓存后再次查询");
user = userService.findById("2");
System.out.println("user = " + user);
}

@Test
public void testDeleteById(){
userService.deleteById("2");
}
}

测试结果:

  • FindAll第一次查询:
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
id = com.study.dao.UserDao
key.toString() = 823101175:176818748:com.study.dao.UserDao.findAll:0:2147483647:select id,name,age,sex,bir,address from t_user:SqlSessionFactoryBean
2022-10-06 00:35:42.005 DEBUG 25808 --- [ main] com.study.dao.UserDao : Cache Hit Ratio [com.study.dao.UserDao]: 0.0
2022-10-06 00:35:42.057 INFO 25808 --- [ main] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} inited
2022-10-06 00:35:42.177 DEBUG 25808 --- [ main] com.study.dao.UserDao.findAll : ==> Preparing: select id,name,age,sex,bir,address from t_user
2022-10-06 00:35:42.188 DEBUG 25808 --- [ main] com.study.dao.UserDao.findAll : ==> Parameters:
2022-10-06 00:35:42.197 DEBUG 25808 --- [ main] com.study.dao.UserDao.findAll : <== Total: 8
key = 823101175:176818748:com.study.dao.UserDao.findAll:0:2147483647:select id,name,age,sex,bir,address from t_user:SqlSessionFactoryBean
value = [User(id=1, name=老王, age=10, bir=Tue Feb 27 17:47:08 CST 2018, address=北京), User(id=2, name=小二王, age=20, bir=Fri Mar 02 15:09:37 CST 2018, address=北京金燕龙), User(id=3, name=喜洋洋, age=30, bir=Sat Aug 13 21:17:25 CST 2022, address=羊村), User(id=4, name=传智播客, age=40, bir=Sun Mar 04 12:04:06 CST 2018, address=北京金燕龙), User(id=5, name=老王, age=50, bir=Wed Mar 07 17:37:26 CST 2018, address=北京), User(id=6, name=小马宝莉, age=60, bir=Thu Mar 08 11:44:00 CST 2018, address=北京修正), User(id=8, name=大猪猪, age=70, bir=Sat Aug 13 20:58:16 CST 2022, address=西虹市), User(id=9, name=皮卡丘, age=80, bir=Sat Aug 13 21:03:49 CST 2022, address=黑龙江)]
User(id=1, name=老王, age=10, bir=Tue Feb 27 17:47:08 CST 2018, address=北京)
User(id=2, name=小二王, age=20, bir=Fri Mar 02 15:09:37 CST 2018, address=北京金燕龙)
User(id=3, name=喜洋洋, age=30, bir=Sat Aug 13 21:17:25 CST 2022, address=羊村)
User(id=4, name=传智播客, age=40, bir=Sun Mar 04 12:04:06 CST 2018, address=北京金燕龙)
User(id=5, name=老王, age=50, bir=Wed Mar 07 17:37:26 CST 2018, address=北京)
User(id=6, name=小马宝莉, age=60, bir=Thu Mar 08 11:44:00 CST 2018, address=北京修正)
User(id=8, name=大猪猪, age=70, bir=Sat Aug 13 20:58:16 CST 2022, address=西虹市)
User(id=9, name=皮卡丘, age=80, bir=Sat Aug 13 21:03:49 CST 2022, address=黑龙江)
开启缓存后再次查询
key.toString() = 823101175:176818748:com.study.dao.UserDao.findAll:0:2147483647:select id,name,age,sex,bir,address from t_user:SqlSessionFactoryBean
2022-10-06 00:35:42.209 DEBUG 25808 --- [ main] com.study.dao.UserDao : Cache Hit Ratio [com.study.dao.UserDao]: 0.5
User(id=1, name=老王, age=10, bir=Tue Feb 27 17:47:08 CST 2018, address=北京)
User(id=2, name=小二王, age=20, bir=Fri Mar 02 15:09:37 CST 2018, address=北京金燕龙)
User(id=3, name=喜洋洋, age=30, bir=Sat Aug 13 21:17:25 CST 2022, address=羊村)
User(id=4, name=传智播客, age=40, bir=Sun Mar 04 12:04:06 CST 2018, address=北京金燕龙)
User(id=5, name=老王, age=50, bir=Wed Mar 07 17:37:26 CST 2018, address=北京)
User(id=6, name=小马宝莉, age=60, bir=Thu Mar 08 11:44:00 CST 2018, address=北京修正)
User(id=8, name=大猪猪, age=70, bir=Sat Aug 13 20:58:16 CST 2022, address=西虹市)
User(id=9, name=皮卡丘, age=80, bir=Sat Aug 13 21:03:49 CST 2022, address=黑龙江)

之后查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
id = com.study.dao.UserDao
key.toString() = 823101175:176818748:com.study.dao.UserDao.findAll:0:2147483647:select id,name,age,sex,bir,address from t_user:SqlSessionFactoryBean
2022-10-06 00:36:58.125 DEBUG 27764 --- [ main] com.study.dao.UserDao : Cache Hit Ratio [com.study.dao.UserDao]: 1.0
User(id=1, name=老王, age=10, bir=Tue Feb 27 17:47:08 CST 2018, address=北京)
User(id=2, name=小二王, age=20, bir=Fri Mar 02 15:09:37 CST 2018, address=北京金燕龙)
User(id=3, name=喜洋洋, age=30, bir=Sat Aug 13 21:17:25 CST 2022, address=羊村)
User(id=4, name=传智播客, age=40, bir=Sun Mar 04 12:04:06 CST 2018, address=北京金燕龙)
User(id=5, name=老王, age=50, bir=Wed Mar 07 17:37:26 CST 2018, address=北京)
User(id=6, name=小马宝莉, age=60, bir=Thu Mar 08 11:44:00 CST 2018, address=北京修正)
User(id=8, name=大猪猪, age=70, bir=Sat Aug 13 20:58:16 CST 2022, address=西虹市)
User(id=9, name=皮卡丘, age=80, bir=Sat Aug 13 21:03:49 CST 2022, address=黑龙江)
开启缓存后再次查询
key.toString() = 823101175:176818748:com.study.dao.UserDao.findAll:0:2147483647:select id,name,age,sex,bir,address from t_user:SqlSessionFactoryBean
2022-10-06 00:36:58.130 DEBUG 27764 --- [ main] com.study.dao.UserDao : Cache Hit Ratio [com.study.dao.UserDao]: 1.0
User(id=1, name=老王, age=10, bir=Tue Feb 27 17:47:08 CST 2018, address=北京)
User(id=2, name=小二王, age=20, bir=Fri Mar 02 15:09:37 CST 2018, address=北京金燕龙)
User(id=3, name=喜洋洋, age=30, bir=Sat Aug 13 21:17:25 CST 2022, address=羊村)
User(id=4, name=传智播客, age=40, bir=Sun Mar 04 12:04:06 CST 2018, address=北京金燕龙)
User(id=5, name=老王, age=50, bir=Wed Mar 07 17:37:26 CST 2018, address=北京)
User(id=6, name=小马宝莉, age=60, bir=Thu Mar 08 11:44:00 CST 2018, address=北京修正)
User(id=8, name=大猪猪, age=70, bir=Sat Aug 13 20:58:16 CST 2022, address=西虹市)
User(id=9, name=皮卡丘, age=80, bir=Sat Aug 13 21:03:49 CST 2022, address=黑龙江)
  • FindById第一次查询:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
id = com.study.dao.UserDao
2022-10-06 00:40:56.075 INFO 1896 --- [ main] com.study.test.TestUserService : Started TestUserService in 1.981 seconds (JVM running for 2.58)
key.toString() = 2088317128:4853372865:com.study.dao.UserDao.findById:0:2147483647:select id,name,age,sex,bir,address from t_user where id=?:2:SqlSessionFactoryBean
2022-10-06 00:40:56.927 DEBUG 1896 --- [ main] com.study.dao.UserDao : Cache Hit Ratio [com.study.dao.UserDao]: 0.0
2022-10-06 00:40:56.974 INFO 1896 --- [ main] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} inited
2022-10-06 00:40:57.083 DEBUG 1896 --- [ main] com.study.dao.UserDao.findById : ==> Preparing: select id,name,age,sex,bir,address from t_user where id=?
2022-10-06 00:40:57.093 DEBUG 1896 --- [ main] com.study.dao.UserDao.findById : ==> Parameters: 2(String)
2022-10-06 00:40:57.100 DEBUG 1896 --- [ main] com.study.dao.UserDao.findById : <== Total: 1
key = 2088317128:4853372865:com.study.dao.UserDao.findById:0:2147483647:select id,name,age,sex,bir,address from t_user where id=?:2:SqlSessionFactoryBean
value = [User(id=2, name=小二王, age=20, bir=Fri Mar 02 15:09:37 CST 2018, address=北京金燕龙)]
user = User(id=2, name=小二王, age=20, bir=Fri Mar 02 15:09:37 CST 2018, address=北京金燕龙)
开启缓存后再次查询
key.toString() = 2088317128:4853372865:com.study.dao.UserDao.findById:0:2147483647:select id,name,age,sex,bir,address from t_user where id=?:2:SqlSessionFactoryBean
2022-10-06 00:40:57.111 DEBUG 1896 --- [ main] com.study.dao.UserDao : Cache Hit Ratio [com.study.dao.UserDao]: 0.5
user = User(id=2, name=小二王, age=20, bir=Fri Mar 02 15:09:37 CST 2018, address=北京金燕龙)

之后再查询:

1
2
3
4
5
6
7
8
9
id = com.study.dao.UserDao
2022-10-06 00:41:35.210 INFO 14564 --- [ main] com.study.test.TestUserService : Started TestUserService in 2.012 seconds (JVM running for 2.61)
key.toString() = 2088317128:4853372865:com.study.dao.UserDao.findById:0:2147483647:select id,name,age,sex,bir,address from t_user where id=?:2:SqlSessionFactoryBean
2022-10-06 00:41:36.200 DEBUG 14564 --- [ main] com.study.dao.UserDao : Cache Hit Ratio [com.study.dao.UserDao]: 1.0
user = User(id=2, name=小二王, age=20, bir=Fri Mar 02 15:09:37 CST 2018, address=北京金燕龙)
开启缓存后再次查询
key.toString() = 2088317128:4853372865:com.study.dao.UserDao.findById:0:2147483647:select id,name,age,sex,bir,address from t_user where id=?:2:SqlSessionFactoryBean
2022-10-06 00:41:36.205 DEBUG 14564 --- [ main] com.study.dao.UserDao : Cache Hit Ratio [com.study.dao.UserDao]: 1.0
user = User(id=2, name=小二王, age=20, bir=Fri Mar 02 15:09:37 CST 2018, address=北京金燕龙)
  • 删除测试:
1
2
3
4
5
6
7
id = com.study.dao.UserDao
2022-10-06 00:45:07.281 INFO 22772 --- [ main] com.study.test.TestUserService : Started TestUserService in 1.997 seconds (JVM running for 2.638)
2022-10-06 00:45:07.532 INFO 22772 --- [ main] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} inited
2022-10-06 00:45:07.692 DEBUG 22772 --- [ main] com.study.dao.UserDao.deleteById : ==> Preparing: delete from t_user where id=?
2022-10-06 00:45:07.703 DEBUG 22772 --- [ main] com.study.dao.UserDao.deleteById : ==> Parameters: 2(String)
2022-10-06 00:45:07.704 DEBUG 22772 --- [ main] com.study.dao.UserDao.deleteById : <== Updates: 0
清空缓存

可以看到,使用到了二级缓存,重复的查找只查找了一次,第二次是在缓存中获取到的,redis中的数据如下:

image-20240404152741590

从redis中可以看出,正好两个查询对应有两个键值对,没有任何问题。这两个查询的值已经到我们的redis服务器中了,如果再启动一次测试代码,那么更简单,直接全部在redis服务器中取,一次数据库的查询都不需要。

14.8完善增、改相关操作

IUserService

1
2
3
void save(User user);

void update(User user);

UserServiceImpl

1
2
3
4
5
6
7
8
9
@Override
public void save(User user) {
userDao.save(user);
}

@Override
public void update(User user) {
userDao.update(user);
}

UserDao

1
2
3
void save(User user);

void update(User user);

UserDaoMapper

1
2
3
4
5
6
7
8
9
<!--save-->
<insert id="save" parameterType="User">
insert into t_user values (#{id},#{name},#{age},#{sex},#{bir},#{address})
</insert>

<!--update-->
<update id="update" parameterType="User">
update t_user set name=#{name},age=#{age},sex=#{sex},bir=#{bir},address=#{address} where id=#{id}
</update>

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
public void testSave(){
userService.save(new User()
.setName("小崔")
.setAge(18)
.setSex('男')
.setBir(new Date())
.setAddress("天山人间")
);
}

@Test
public void testUpdate(){
userService.update(new User()
.setId("10")
.setName("猪猪侠")
.setAge(8)
.setSex('男')
.setBir(new Date())
.setAddress("棒棒糖村")
);
}

运行后发现执行增删改操作后,均会清空redis的缓存。

14.9缓存在项目中的应用

image-20240404152946970

当执行增删改操作时,就回调用RedisCache中的clear方法,清空redis中该key对应的所有数据,也就是以mapper文件为单位进行情况,根据id为标志,并不能影响到其他的表。

如果多张表不存在任何的关联查询,就不会出现问题;但如果其中有关联查询,如果修改了其中一个表,紧接着删除了该表的所有缓存,但另一张关联的表是不会清除缓存的,这时就会出现查询到的数据与数据库不统一的情况。

这时候就需要修改一些,使得我们有关联关系的两张表增删改其中的一张表时,就清空所有的关联表的缓存,不仅仅删除自身的缓存。所有我们就不能在每张表都设置一个cache标签了,让两个有关联关系的表共用一个RedisCache对象,如下:

image-20240404153026469

只需要一个cache-ref标签即可,这样的话这两个mapper的缓存就在一起了,清空也是全清空。

14.10缓存优化策略

image-20240404153053927

一般Redis中单个key、value长度大小原则上不超过1GB。这一步主要是对缓存的键值对的一个优化措施,从上面的图片可以看到,key对应于一条sql语句加上其他的一些信息,看起来很冗长,这样会影响redis的性能,我们要尽可能设计的简洁一下。

我们的目的是让key变短,且必须是唯一的,不能够冲突,这一特点可以使用加密算法那一模板的报文摘要技术,把一个长的数据变为一个固定长度的数据,且能够唯一的区分。最常用的报文摘要就是即MD5,我们就将key进行MD5加密,再存放到redis服务器中。写为这种形式:

image-20240404153129328

优化RedisCache:

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
package com.study.cache;

import com.study.util.ApplicationContextUtils;
import org.apache.ibatis.cache.Cache;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.util.DigestUtils;

/**
* @ClassName RedisCache
* @Description 自定义RedisCache缓存实现
* @Author chabai
* @Date 2022/10/5 23:14
* @Version 1.0
*/
public class RedisCache implements Cache {
// 必须指定String类型的id,即:当前放入缓存的mapper的namespace
private final String id;

// 必须提供以String类型的id作为入参的构造方法
public RedisCache(String id){
System.out.println("id = " + id);// id = com.study.dao.UserDao
this.id = id;
}

// 返回cache的唯一标识
@Override
public String getId() {
return this.id;
}

/**
* @MethodName putObject
* @Description 缓存中放入数据
* @param: key
* @param: value
* @Author chabai
* @Date 23:27 2022/10/5
*/
@Override
public void putObject(Object key, Object value) {
// 使用redisHash类型作为缓存存储模型
getRedisTemplate().opsForHash().put(id.toString(),getKeyToMD5(key.toString()),value);
}

/**
* @MethodName getObject
* @Description 缓存中获取数据
* @param: key
* @return: java.lang.Object
* @Author chabai
* @Date 23:58 2022/10/5
*/
@Override
public Object getObject(Object key) {
return getRedisTemplate().opsForHash().get(id.toString(),getKeyToMD5(key.toString()));
}

/**
* @MethodName removeObject
* @Description 根据指定key删除缓存
* @param: key
* @return: java.lang.Object
* @Author chabai
* @Date 0:19 2022/10/6
*/
@Override
public Object removeObject(Object key) {
return getRedisTemplate().opsForHash().delete(id.toString(),getKeyToMD5(key.toString()));
}

/**
* @MethodName clear
* @Description 清空所有缓存:增删改时执行
* @Author chabai
* @Date 13:15 2022/10/6
*/
@Override
public void clear() {
getRedisTemplate().delete(id.toString());
}

/**
* @MethodName getSize
* @Description 用来计算缓存数量
* @return: int
* @Author chabai
* @Date 0:24 2022/10/6
*/
@Override
public int getSize() {
return getRedisTemplate().opsForHash().size(id.toString()).intValue();// long to int
}

/**
* @MethodName 封装redisTemplate
* @Description
* @Author chabai
* @Date 13:16 2022/10/6
*/
private RedisTemplate getRedisTemplate(){
// 通过application工具类获取RedisTemplate
RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
// 设置序列化方式
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
return redisTemplate;
}

/**
* @MethodName getKeyToMD5
* @Description 封装一个对key进行md5加密处理的方法,用于简化缓存数据存储时key的长度
* @param: key
* @return: java.lang.String
* @Author chabai
* @Date 16:35 2022/10/6
*/
private String getKeyToMD5(String key){
return DigestUtils.md5DigestAsHex(key.getBytes());
}
}

重新清空redis,并运行查询后结果如下:

image-20240404153217220

实测结果:

image-20240404153236655

键名的长度得到了明显的缩减,查找速度也会变快一点,相应的也能给缓存设置一个超时时间。

14.11最终项目目录结构

image-20240404153303196

14.12缓存相关面试题

14.12.1缓存穿透

缓存穿透:是指客户端请求的数据在缓存中和数据库中都不存在(如查找id为-1的数据),这样缓存永远不会生效,这些恶意请求都会到达数据库,让数据库承受巨大的压力,严重情况下会使服务器宕机。

注意:mybatis中cache解决了缓存穿透,将数据库中没有查询到的j结果也进行缓存,下次再有相同查找时直接从缓冲中返回空数据。
有常用的两种解决方法:

  • 缓存空对象:把查到为空的数据也缓存在缓存中,下次再有相同的形式查找就会从缓存中拿到空数据。

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

    • 缺点:

      1.额外的内存消耗(可能会恶意使用随机值查找),解决方法为设置超时时间;

      2.可能造成短期的不一致

  • 布隆过滤:请求时先访问布隆过滤器,如果存在,要查的数据就放行;如果不存在,就拒绝。优点:内存占用小,没有多余key

    缺点:可能误判,实现复杂

  • 实时监控:发现Redis的命中率变低了,就进行排查。
    image-20240404153516607

另外,还可以通过增强id的复杂度,避免被操作id的规律、做好基础校验等主动的解决方案。

14.12.2缓存雪崩

缓存雪崩:是指在某一时期,大量的缓存同时失效或者redis服务器宕机,导致大量的请求到达数据库,带来巨大的压力,使数据库阻塞或挂起。

举例:

1
2
// 缓存超时 所有业务同一时刻缓存全部失效,极有可能出现缓存雪崩情况
getRedisTemplate().expire(id.toString(),1, TimeUnit.HOURS);

解决方案:

  • 缓存永久存储,不推荐

  • 给不同的缓存添加不同的过期时间(常用的数据ttl更长,冷门的ttl更短)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    if(id.equals("com.baizhi.dao.UserDAO")){
    //缓存超时 client 用户 client 员工
    getRedisTemplate().expire(id.toString(),1, TimeUnit.HOURS);
    }


    if(id.equals("com.baizhi.dao.CityDAO")){
    //缓存超时 client 用户 client 员工
    getRedisTemplate().expire(id.toString(),30, TimeUnit.MINUTES);
    }

    //.....指定不同业务模块设置不同缓存超时时间
  • 利用rediis集群提高服务的可用性,防止宕机

  • 给缓存业务添加降级限流策略

  • 给业务添加多级缓存,nginx缓存+redis缓存+其他缓存

14.12.3 缓存击穿

缓存击穿:也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。和雪崩的区别就是不是大量的key过期,redis还是正常状态,但数据库却崩了。

常用解决方案:

  • 互斥锁:同时只让一个线程查询数据库,其他的都等待从缓存中取,实现简单,强一致性,但性能差,可用性变低了。

  • 逻辑过期:永久存储该数据,但在数据中额外存储一个逻辑的过期时间。取到该数据时检测到过期了,那还是返回该数据,只不过会再新开一个线程去同步数据库的新数据到缓存。最终一致性,实现复杂。

  • 预先设置热门数据:在redis高峰访问之前,把热门的数据提前存到redis里,并加大热门数据的ttl。

  • 实时调整:监控热门数据,调整key的ttl

image-20240404153808432

image-20240404153817022