spring boot

spring boot cache

Cacheable注解配置的两项参数说明:

value:缓存的名称,缓存名称作为缓存key的前缀。

key: 缓存key,支持SpEL表达式,上述代码表示取参数id的值作为key

最终缓存key为:缓存名称+“::”+key,例如:上述代码id为123,最终的key为:JZ_CACHE:SERVE_RECORD::123

SpEL(Spring Expression Language)是一种在 Spring 框架中用于处理字符串表达式的强大工具,它可以实现获取对象的属性,调用对象的方法操作。

keyGenerator:指定一个自定义的键生成器(实现 org.springframework.cache.interceptor.KeyGenerator 接口的类),用于生成缓存的键。与 key 属性互斥,二者只能选其一。

unless

1
2
3
4
5
// 返回数据为空 缓存
// unless true 不缓存
// @Cacheable(value = "", key = "", cacheManager = "", unless = "#result.size() != 0")
// 返回数据不为空 缓存
// @Cacheable(value = "", key = "", cacheManager = "", unless = "#result.size() == 0")
1
2
3
4
5
6
7
8
9
10
11
12
13
@EnableCaching:开启缓存注解功能

@Cacheable:查询数据时缓存,将方法的返回值进行缓存。
// 缓存key,支持SpEL表达式(spring boot 提供 ,支持 取对象属性及执行属性方法),上述代码表示取参数id的值作为key
// 最终缓存key为:缓存名称+“::”+key
@Cacheable(value = RedisConstants.CacheName.SERVE, key = "#id", cacheManager =
RedisConstants.CacheManager.ONE_DAY)
/**
* 缓存时间1天 方法名
*/
public static final String ONE_DAY = "cacheManagerOneDay";


sping cache RedisCacheManager 方法:

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
package com.jzo2o.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 com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.jzo2o.common.utils.DateUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
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.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;

import java.math.BigInteger;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Random;

/**
* SpringCache配置
*
* @author itcast
* @create 2023/8/15 10:04
**/
@Configuration
public class SpringCacheConfig {

/**
* 缓存时间30分钟
*
* @param connectionFactory redis连接工厂
* @return redis缓存管理器
*/
@Bean
public RedisCacheManager cacheManager30Minutes(RedisConnectionFactory connectionFactory) {
int randomNum = new Random().nextInt(100);
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(30 * 60L + randomNum))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(JACKSON_SERIALIZER));

return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.transactionAware()
.build();
}

/**
* 缓存时间1天
*
* @param connectionFactory redis连接工厂
* @return redis缓存管理器
*/
@Bean
public RedisCacheManager cacheManagerOneDay(RedisConnectionFactory connectionFactory) {
//生成随机数
int randomNum = new Random().nextInt(6000);
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
//过期时间为基础时间加随机数
.entryTtl(Duration.ofSeconds(24 * 60 * 60L + randomNum))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(JACKSON_SERIALIZER));

return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.transactionAware()
.build();
}

/**
* 永久缓存
*
* @param connectionFactory redis连接工厂
* @return redis缓存管理器
*/
@Bean
@Primary
public RedisCacheManager cacheManagerForever(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(JACKSON_SERIALIZER));

return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.transactionAware()
.build();
}

private static final Jackson2JsonRedisSerializer<Object> JACKSON_SERIALIZER;

static {
//定义Jackson类型序列化对象
JACKSON_SERIALIZER = new Jackson2JsonRedisSerializer<>(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();

// SimpleModule对象,添加各种序列化器和反序列化器。解决LocalDateTime、Long序列化异常
SimpleModule simpleModule = new SimpleModule()
// 添加反序列化器
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DateUtils.DEFAULT_DATE_TIME_FORMAT)))
.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DateUtils.DEFAULT_DATE_FORMAT)))
// 添加序列化器
.addSerializer(BigInteger.class, ToStringSerializer.instance)
.addSerializer(Long.class, ToStringSerializer.instance) // 实现 Long --> String 的序列化器
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DateUtils.DEFAULT_DATE_TIME_FORMAT)))
.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DateUtils.DEFAULT_DATE_FORMAT)));
om.registerModule(simpleModule);
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL
, JsonTypeInfo.As.WRAPPER_ARRAY);
JACKSON_SERIALIZER.setObjectMapper(om);
}
}

工作原理

Spring Cache是基于AOP原理,对添加注解@Cacheable的类生成代理对象,在方法执行前查看是否有缓存对应的数据,如果有直接返回数据,如果没有调用源方法获取数据返回,并缓存起来,下边跟踪Spring Cache的切面类CacheAspectSupport.java中的private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts)方法。

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
  @Nullable
private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
...
Object cacheValue;
Object returnValue;
if (cacheHit != null && !this.hasCachePut(contexts)) {
cacheValue = cacheHit.get();
returnValue = this.wrapCacheValue(method, cacheValue);
} else {
returnValue = this.invokeOperation(invoker);
cacheValue = this.unwrapReturnValue(returnValue);
}

this.collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
Iterator var8 = cachePutRequests.iterator();

while(var8.hasNext()) {
CachePutRequest cachePutRequest = (CachePutRequest)var8.next();
cachePutRequest.apply(cacheValue);
}

this.processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);
return returnValue;
}

1
2
3
4
@CacheEvict:用于删除缓存,将一条或多条数据从缓存中删除。
@CachePut:用于更新缓存,将方法的返回值放到缓存中
@Caching:组合多个缓存注解;
@CacheConfig:统一配置@Cacheable中的value值

缓存穿透

大量用户访问一个不存在的资源

查询一个缓存中不存在的数据将会执行方法查询数据库,数据库也不存在此数据,查询完数据库也没有缓存数据,缓存没有起到作用。

解决方案

  1. 访问数据库不存在的资源时,缓存一个空值或特殊值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Cacheable(
    value = "users",
    key = "#id",
    unless = "#result == null" // 默认不缓存null,需要特殊处理
    )
    public User getUserById(Long id) {
    User user = userRepository.findById(id).orElse(null);
    return user != null ? user : new NullUser(); // 返回特殊空对象
    }
  2. 布隆过滤器

    布隆过滤器(Bloom Filter)是一种数据结构,用于快速判断一个元素是否属于一个集合中。

    它使用多个Hash函数将一个元素映射成一个位阵列(Bit array)中的一个点,将Bit array理解为一个二进制数组,数组元素是0或1。

    常见框架:

    • 使用redit的bitmap位图结构实现。

    • redisson

    • google的Guava库实现。

  3. 请求校验

缓存击穿

访问热点数据,大量访问同一个热点数据,当热点数据失效后同时请求数据库,数据库宕机。

解决方案

  • 单体架构下(单进程内)可以使用同步锁控制查询数据库的代码,只允许有一个线程去查询数据库,查询得到数据库存入缓存。
  • 分布式架构下(多个进程之间)可以使用分布式锁进行控制。
  1. 分布式锁

    redisson

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 获取分布式锁对象
    RLock lock = redisson.getLock("myLock");
    try {
    // 尝试加锁,最多等待100秒,加锁后自动解锁时间为30秒
    boolean isLocked = lock.tryLock(100, 30, java.util.concurrent.TimeUnit.SECONDS);
    if (isLocked) {
    //查询数据库
    //存入缓存
    } else {
    System.out.println("获取锁失败,可能有其他线程持有锁");
    }
    } catch (InterruptedException e) {
    e.printStackTrace();
    } finally {
    // 释放锁
    lock.unlock();
    System.out.println("释放锁...");
    }
  2. 缓存预热

    • 提前预热
    • 定时预热
  3. 热点数据查询降级处理

    对热点数据查询定义单独的接口,当缓存中不存在时走降级方法避免查询数据库。

缓存雪崩

缓存雪崩是缓存中大量key失效后当高并发到来时导致大量请求到数据库,瞬间耗尽数据库资源,导致数据库无法使用。

比如对某信息设置缓存过期时间为30分钟,在大量请求同时查询该类信息时,此时就会有大量的同类信息存在相同的过期时间,一旦失效将同时失效,造成雪崩问题。

解决方案

  1. 使用锁控制

    同 缓存击穿

  2. 对同一类型信息的key设置不同的过期时间

    具体实现:在framework工程中定义缓存管理器指定过期时间加上随机数

  3. 缓存定时预热

    不用等到请求到来再去查询数据库存入缓存,可以提前将数据存入缓存。使用缓存预热机制通常有专门的后台程序去将数据库的数据同步到缓存。

缓存不一致

写数据库,写缓存(redis)不一致,双写不一致

解决方案

  1. 分布式锁

    流程:

    线程1申请分布式锁,拿到锁。此时其它线程无法获取同一把锁。

    线程1写数据库,写缓存,操作完成释放锁。

    线程2申请分布锁成功,写数据库,写缓存。

    对双写的操作每个线程顺序执行。

    对操作异常问题仍需要解决:写数据库成功写缓存失败了,数据库需要回滚,此时就需要使用分布式事务组件。

    使用分布式锁解决双写一致性不仅性能低下,复杂度增加。

  2. 延迟双删

    线程1先删除缓存,再写入主数据库,延迟一定时间再删除缓存。

    保证 最终一致

主从数据库

MySQL主从集群由MySQL主服务器(master)和MySQL从服务器(slave)组成,MySQL主从数据同步是一种数据库复制技术,进行写数据会先向主服务器写,写成功后将数据同步到从服务器,流程如下:

1、主服务器将所有写操作(INSERT、UPDATE、DELETE)以二进制日志(binlog)的形式记录下来。

2、从服务器连接到主服务器,发送dump 协议,请求获取主服务器上的binlog日志。

MySQL的dump协议是MySQL复制协议中的一部分。

3、MySQL master 收到 dump 请求,开始推送 binary log 给 slave

4、从服务器解析日志,根据日志内容更新从服务器的数据库,完成从服务器的数据保持与主服务器同步。

Canal

Canal是什么?

**canal [kə’næl]**,译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,对数据进行同步,如下图:

Canal可与很多数据源进行对接,将数据由MySQL同步到ES、MQ、DB等各个数据源。

Canal的意思是水道/管道/沟渠,它相当于一个数据管道,通过解析MySQL的binlog日志完成数据同步工作。

官方文档:https://github.com/alibaba/canal/wiki

工作流程

1、Canal模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议

MySQL的dump协议是MySQL复制协议中的一部分。

2、MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal )

。一旦连接建立成功,Canal会一直等待并监听来自MySQL主服务器的binlog事件流,当有新的数据库变更发生时MySQL master主服务器发送binlog事件流给Canal。

3、Canal会及时接收并解析这些变更事件并解析 binary log

通过以上流程可知Canal和MySQL master主服务器之间建立了长连接。