一、缓存

1、本地缓存

1.1 使用hashmap本地缓存

1
2
3
4
5
6
7
8
9
10
11
12
//测试本地缓存,通过hashmap
private Map<String,Object> cache=new HashMap<>();

public Map<String, List<Catalog2Vo>> getCategoryMap() {
Map<String, List<Catalog2Vo>> catalogMap = (Map<String, List<Catalog2Vo>>) cache.get("catalogMap");
//如果没有缓存,则从数据库中查询并放入缓存中
if (catalogMap == null) {
catalogMap = getCategoriesDb();
cache.put("catalogMap",catalogMap);
}
return catalogMap;
}

1.2 整合redis进行测试

导入依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置redis主机地址

1
2
3
4
spring:
redis:
host: 192.168.56.10
port: 6379

使用springboot自动配置的RedisTemplate优化菜单获取业务

1
2
3
4
5
6
7
8
9
10
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
String catalogJson = ops.get("catalogJson");
if (catalogJson == null) {
Map<String, List<Catalog2Vo>> categoriesDb = getCategoriesDb();
String toJSONString = JSON.toJSONString(categoriesDb);
ops.set("catalogJson",toJSONString);
return categoriesDb;
}
Map<String, List<Catalog2Vo>> listMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2Vo>>>() {});
return listMap;

内存泄漏及解决办法

当进行压力测试时后期后出现堆外内存溢出OutOfDirectMemoryError

产生原因:

1)、springboot2.0以后默认使用lettuce操作redis的客户端,它使用通信

2)、lettuce的bug导致netty堆外内存溢出

解决方案:由于是lettuce的bug造成,不能直接使用-Dio.netty.maxDirectMemory去调大虚拟机堆外内存

1)、升级lettuce客户端。 2)、切换使用jedis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>

1.3 高并发下缓存失效问题

缓存击穿

只查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,将失去缓存的意义

风险:

​ 利用不存在的数据进行攻击,数据库瞬间压力增大,最终导致崩溃

解决:

​ null结果缓存,并加入短暂过期时间

缓存雪崩

​ 缓存雪崩是指我们设置缓存时key采用了相同的过期时间,导致缓存在某一时刻失效,请求全部转发到DB,DB瞬间压力过大雪崩。

解决:

​ 将原有的失效时间基础上增加一个随机值,比如1-5分钟的随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

缓存击穿

  • 对于一些设置过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。
  • 如果这个key在大量请求同时进行前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿

解决:

​ 加锁。大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去db。

1.4 加锁解决缓存击穿问题

将查询db的方法加锁,这样在同一时间只有一个方法能查询数据库,就能解决缓存击穿的问题了

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
public Map<String, List<Catalog2Vo>> getCategoryMap() {
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
String catalogJson = ops.get("catalogJson");
if (StringUtils.isEmpty(catalogJson)) {
System.out.println("缓存不命中,准备查询数据库。。。");
Map<String, List<Catalog2Vo>> categoriesDb = getCategoriesDb();
String toJSONString = JSON.toJSONString(categoriesDb);
ops.set("catalogJson",toJSONString);
return categoriesDb;
}
System.out.println("缓存命中。。。。");
Map<String, List<Catalog2Vo>> listMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2Vo>>>() {});
return listMap;
}

private synchronized Map<String, List<Catalog2Vo>> getCategoriesDb() {
String catalogJson = stringRedisTemplate.opsForValue().get("catalogJson");
if (StringUtils.isEmpty(catalogJson)) {
System.out.println("查询了数据库");
。。。。。
return listMap;
}else {
Map<String, List<Catalog2Vo>> listMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2Vo>>>() {});
return listMap;
}
}

1.5 锁时序问题

在上述方法中,我们将业务逻辑中的确认缓存没有查数据库放到了锁里,但是最终控制台却打印了两次查询了数据库。这是因为在将结果放入缓存的这段时间里,有其他线程确认缓存没有,又再次查询了数据库,因此我们要将结果放入缓存也进行加锁

优化代码逻辑后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public Map<String, List<Catalog2Vo>> getCategoryMap() {
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
String catalogJson = ops.get("catalogJson");
if (StringUtils.isEmpty(catalogJson)) {
System.out.println("缓存不命中,准备查询数据库。。。");
synchronized (this) {
String synCatalogJson = stringRedisTemplate.opsForValue().get("catalogJson");
if (StringUtils.isEmpty(synCatalogJson)) {
Map<String, List<Catalog2Vo>> categoriesDb= getCategoriesDb();
String toJSONString = JSON.toJSONString(categoriesDb);
ops.set("catalogJson", toJSONString);
return categoriesDb;
}else {
Map<String, List<Catalog2Vo>> listMap = JSON.parseObject(synCatalogJson, new TypeReference<Map<String, List<Catalog2Vo>>>() {});
return listMap;
}
}
}
System.out.println("缓存命中。。。。");
Map<String, List<Catalog2Vo>> listMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2Vo>>>() {});
return listMap;
}

优化后多线程访问时仅查询一次数据库

2、分布式缓存

2.1 本地缓存面临问题

当有多个服务存在时,每个服务的缓存仅能够为本服务使用,这样每个服务都要查询一次数据库,并且当数据更新时只会更新单个服务的缓存数据,就会造成数据不一致的问题

所有的服务都到同一个redis进行获取数据,就可以避免这个问题

2.2 分布式锁

当分布式项目在高并发下也需要加锁,但本地锁只能锁住当前服务,这个时候就需要分布式锁

2.3 分布式锁的演进

基本原理

我们可以同时去一个地方“占坑”,如果占到,就执行逻辑。否则就必须等待,直到释放锁。“占坑”可以去redis,可以去数据库,可以去任何大家都能访问的地方。等待可以自旋的方式。

阶段一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
	public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {
//阶段一
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111");
//获取到锁,执行业务
if (lock) {
Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap();
//删除锁,如果在此之前报错或宕机会造成死锁
stringRedisTemplate.delete("lock");
return categoriesDb;
}else {
//没获取到锁,等待100ms重试
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCatalogJsonDbWithRedisLock();
}
}

public Map<String, List<Catalog2Vo>> getCategoryMap() {
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
String catalogJson = ops.get("catalogJson");
if (StringUtils.isEmpty(catalogJson)) {
System.out.println("缓存不命中,准备查询数据库。。。");
Map<String, List<Catalog2Vo>> categoriesDb= getCategoriesDb();
String toJSONString = JSON.toJSONString(categoriesDb);
ops.set("catalogJson", toJSONString);
return categoriesDb;
}
System.out.println("缓存命中。。。。");
Map<String, List<Catalog2Vo>> listMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2Vo>>>() {});
return listMap;
}

问题:
1、setnx占好了位,业务代码异常或者程序在页面过程中宕机。没有执行删除锁逻辑,这就造成了死锁

解决:设置锁的自动过期,即使没有删除,会自动删除

阶段二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111");
if (lock) {
//设置过期时间
stringRedisTemplate.expire("lock", 30, TimeUnit.SECONDS);
Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap();
stringRedisTemplate.delete("lock");
return categoriesDb;
}else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCatalogJsonDbWithRedisLock();
}
}

问题:
1、setnx设置好,正要去设置过期时间,宕机。又死锁了。
解决:
设置过期时间和占位必须是原子的。redis支持使用setnx ex命令

阶段三

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {
//加锁的同时设置过期时间,二者是原子性操作
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "1111",5, TimeUnit.SECONDS);
if (lock) {
Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap();
//模拟超长的业务执行时间
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
stringRedisTemplate.delete("lock");
return categoriesDb;
}else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCatalogJsonDbWithRedisLock();
}
}

问题:
1、删除锁直接删除???
如果由于业务时间很长,锁自己过期了,我们直接删除,有可能把别人正在持有的锁删除了。
解决:
占锁的时候,值指定为uuid,每个人匹配是自己的锁才删除。

阶段四

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
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {
String uuid = UUID.randomUUID().toString();
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
//为当前锁设置唯一的uuid,只有当uuid相同时才会进行删除锁的操作
Boolean lock = ops.setIfAbsent("lock", uuid,5, TimeUnit.SECONDS);
if (lock) {
Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap();
String lockValue = ops.get("lock");
if (lockValue.equals(uuid)) {
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
stringRedisTemplate.delete("lock");
}
return categoriesDb;
}else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCatalogJsonDbWithRedisLock();
}
}

问题:
1、如果正好判断是当前值,正要删除锁的时候,锁已经过期,别人已经设置到了新的值。那么我们删除的是别人的锁
解决:
删除锁必须保证原子性。使用redis+Lua脚本完成

阶段五-最终形态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {
String uuid = UUID.randomUUID().toString();
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
Boolean lock = ops.setIfAbsent("lock", uuid,5, TimeUnit.SECONDS);
if (lock) {
Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap();
String lockValue = ops.get("lock");
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), lockValue);
return categoriesDb;
}else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCatalogJsonDbWithRedisLock();
}
}

保证加锁【占位+过期时间】和删除锁【判断+删除】的原子性。更难的事情,锁的自动续期

2.4 Redisson

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。

本文我们仅关注分布式锁的实现,更多请参考官方文档

2.4.1 环境搭建

导入依赖

1
2
3
4
5
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>

开启配置

1
2
3
4
5
6
7
8
9
10
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.56.102:6379");
RedissonClient redisson = Redisson.create(config);
return redisson;
}
}
2.4.2 可重入锁(Reentrant Lock)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisson() {
Map<String, List<Catalog2Vo>> categoryMap=null;
RLock lock = redissonClient.getLock("CatalogJson-Lock");
lock.lock();
try {
Thread.sleep(30000);
categoryMap = getCategoryMap();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
return categoryMap;
}
}

如果负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,所以就设置了过期时间,但是如果业务执行时间过长,业务还未执行完锁就已经过期,那么就会出现解锁时解了其他线程的锁的情况。

所以Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。

在本次测试中CatalogJson-Lock的初始过期时间TTL为30s,但是每到20s就会自动续借成30s

另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。不会自动续期!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 加锁以后10秒钟自动解锁,看门狗不续命
// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);

// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
...
} finally {
lock.unlock();
}
}
如果传递了锁的超时时间,就执行脚本,进行占锁;
如果没传递锁时间,使用看门狗的时间,占锁。如果返回占锁成功future,调用future.onComplete();
没异常的话调用scheduleExpirationRenewal(threadId);
重新设置过期时间,定时任务;
看门狗的原理是定时任务:重新给锁设置过期时间,新的过期时间就是看门狗的默认时间;
锁时间/3是定时任务周期

edisson同时还为分布式锁提供了异步执行的相关方法:

1
2
3
4
RLock lock = redisson.getLock("anyLock");
lock.lockAsync();
lock.lockAsync(10, TimeUnit.SECONDS);
Future<Boolean> res = lock.tryLockAsync(100, 10, TimeUnit.SECONDS);

RLock对象完全符合Java的Lock规范。也就是说只有拥有锁的进程才能解锁,其他进程解锁则会抛出IllegalMonitorStateException错误。但是如果遇到需要其他进程也能解锁的情况,请使用分布式信号量Semaphore 对象.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisson() {
Map<String, List<Catalog2Vo>> categoryMap=null;
RLock lock = redissonClient.getLock("CatalogJson-Lock");
lock.lock();
try {
Thread.sleep(30000);
categoryMap = getCategoryMap();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
return categoryMap;
}
}

最佳实战:自己指定锁时间,时间长点即可

2.4.3 读写锁(ReadWriteLock)
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
@GetMapping("/read")
@ResponseBody
public String read() {
RReadWriteLock lock = redissonClient.getReadWriteLock("ReadWrite-Lock");
RLock rLock = lock.readLock();
String s = "";
try {
rLock.lock();
System.out.println("读锁加锁"+Thread.currentThread().getId());
Thread.sleep(5000);
s= redisTemplate.opsForValue().get("lock-value");
}finally {
rLock.unlock();
return "读取完成:"+s;
}
}

@GetMapping("/write")
@ResponseBody
public String write() {
RReadWriteLock lock = redissonClient.getReadWriteLock("ReadWrite-Lock");
RLock wLock = lock.writeLock();
String s = UUID.randomUUID().toString();
try {
wLock.lock();
System.out.println("写锁加锁"+Thread.currentThread().getId());
Thread.sleep(10000);
redisTemplate.opsForValue().set("lock-value",s);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
wLock.unlock();
return "写入完成:"+s;
}
}

写锁会阻塞读锁,但是读锁不会阻塞读锁,但读锁会阻塞写锁

总之含有写的过程都会被阻塞,只有读读不会被阻塞

上锁时在redis的状态

2.4.4 信号量(Semaphore)

信号量为存储在redis中的一个数字,当这个数字大于0时,即可以调用acquire()方法增加数量,也可以调用release()方法减少数量,但是当调用release()之后小于0的话方法就会阻塞,直到数字大于0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@GetMapping("/park")
@ResponseBody
public String park() {
RSemaphore park = redissonClient.getSemaphore("park");
try {
park.acquire(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "停进2";
}

@GetMapping("/go")
@ResponseBody
public String go() {
RSemaphore park = redissonClient.getSemaphore("park");
park.release(2);
return "开走2";
}
2.4.5 闭锁(CountDownLatch)

可以理解为门栓,使用若干个门栓将当前方法阻塞,只有当全部门栓都被放开时,当前方法才能继续执行。

以下代码只有offLatch()被调用5次后 setLatch()才能继续执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@GetMapping("/setLatch")
@ResponseBody
public String setLatch() {
RCountDownLatch latch = redissonClient.getCountDownLatch("CountDownLatch");
try {
latch.trySetCount(5);
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
return "门栓被放开";
}

@GetMapping("/offLatch")
@ResponseBody
public String offLatch() {
RCountDownLatch latch = redissonClient.getCountDownLatch("CountDownLatch");
latch.countDown();
return "门栓被放开1";
}

闭锁在redis的存储状态

3、缓存数据的一致性

3.1 双写模式

当数据更新时,更新数据库时同时更新缓存

存在问题

由于卡顿等原因,导致写缓存2在最前,写缓存1在后面就出现了不一致

这是暂时性的脏数据问题,但是在数据稳定,缓存过期以后,又能得到最新的正确数据

3.2 失效模式

数据库更新时将缓存删除

存在问题

当两个请求同时修改数据库,一个请求已经更新成功并删除缓存时又有读数据的请求进来,这时候发现缓存中无数据就去数据库中查询并放入缓存,在放入缓存前第二个更新数据库的请求成功,这时候留在缓存中的数据依然是第一次数据更新的数据

解决方法

1、缓存的所有数据都有过期时间,数据过期下一次查询触发主动更新
2、读写数据的时候(并且写的不频繁),加上分布式的读写锁。

3.3 解决方案

无论是双写模式还是失效模式,都会导致缓存的不一致问题。即多个实例同时更新会出事。怎么办?

  • 如果是用户纬度数据(订单数据、用户数据),这种并发几率非常小,不用考虑这个问题,缓存数据加上过期时间,每隔一段时间触发读的主动更新即可
  • 如果是菜单,商品介绍等基础数据,也可以去使用canal订阅binlog的方式。
  • 缓存数据+过期时间也足够解决大部分业务对于缓存的要求。
  • 通过加锁保证并发读写,写写的时候按顺序排好队。读读无所谓。所以适合使用读写锁。(业务不关心
    脏数据,允许临时脏数据可忽略);

总结:

  • 我们能放入缓存的数据本就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保
    证每天拿到当前最新数据即可。
  • 我们不应该过度设计,增加系统的复杂性
  • 遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点。

4、 SpringCache

这部分可以参考我之前的学习笔记:https://oy6090.top/posts/3830795892/

4.1 导入依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>

4.2 自定义配置

指定缓存类型并在主配置类上加上注解@EnableCaching

1
2
3
4
5
6
7
8
9
10
11
12
spring:
cache:
#指定缓存类型为redis
type: redis
redis:
#指定redis中的过期时间为1h
time-to-live: 3600000
#如果指定了前缀就用我们指定的前缀,如果没有就默认使用缓存的名字作为前缀
#key-prefix: CACHE_
use-key-prefix: true
#是否缓存空值,防止缓存穿透
cache-null-values: true

默认使用jdk进行序列化,自定义序列化方式需要编写配置类

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
@EnableConfigurationProperties(CacheProperties.class)
@Configuration
@EnableCaching
public class MyCacheConfig {

/**
* 配置文件的配置没有用上
* 1. 原来和配置文件绑定的配置类为:@ConfigurationProperties(prefix = "spring.cache")
* public class CacheProperties
* <p>
* 2. 要让他生效,要加上 @EnableConfigurationProperties(CacheProperties.class)
*/
@Bean
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
// config = config.entryTtl();
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

CacheProperties.Redis redisProperties = cacheProperties.getRedis();
//将配置文件中所有的配置都生效
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixKeysWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}

4.3 自定义序列化原理

缓存使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 	//调用该方法时会将结果缓存,缓存名为category,key为方法名
//表示该方法的缓存被读取时会加锁
@Cacheable(value = {"category"},key = "#root.methodName",sync = true)
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithSpringCache() {
return getCategoriesDb();
}

//调用该方法会删除缓存category下的所有cache
@Override
@CacheEvict(value = {"category"},allEntries = true)
public void updateCascade(CategoryEntity category) {
this.updateById(category);
if (!StringUtils.isEmpty(category.getName())) {
categoryBrandRelationService.updateCategory(category);
}
}

第一个方法缓存结果后

第二个方法调用清除缓存后

4.4 Spring-Cache的不足之处

  1. 读模式
  • 缓存穿透:查询一个null数据。解决方案:缓存空数据,可通过spring.cache.redis.cache-null-values=true

  • 缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:加锁 ? 默认是无加锁的;使用sync = true来解决击穿问题

    image-20210822003529266

  • 缓存雪崩:大量的key同时过期。解决:加随机时间。加上过期时间

    image-20210822003556256

  1. 写模式:(缓存与数据库一致)
  • 读写加锁。

  • 引入Canal,感知到MySQL的更新去更新Redis

  • 读多写多,直接去数据库查询就行

  1. 总结:

    常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache):

    写模式(只要缓存的数据有过期时间就足够了)

    特殊数据:特殊设计