spring boot + spring cache 实现两级缓存(redis + caffeine)

释放双眼,带上耳机,听听看~!

spring boot中集成了spring cache,并有多种缓存方式的实现,如:Redis、Caffeine、JCache、EhCache等等。但如果只用一种缓存,要么会有较大的网络消耗(如Redis),要么就是内存占用太大(如Caffeine这种应用内存缓存)。在很多场景下,可以结合起来实现一、二级缓存的方式,能够很大程度提高应用的处理效率。

内容说明:

  • 缓存、两级缓存
  • spring cache:主要包含spring cache定义的接口方法说明和注解中的属性说明
  • spring boot + spring cache:RedisCache实现中的缺陷
  • caffeine简介
  • spring boot + spring cache 实现两级缓存(redis + caffeine)

缓存、两级缓存


简单的理解,缓存就是将数据从读取较慢的介质上读取出来放到读取较快的介质上,如磁盘–>内存。平时我们会将数据存储到磁盘上,如:数据库。如果每次都从数据库里去读取,会因为磁盘本身的IO影响读取速度,所以就有了像redis这种的内存缓存。可以将数据读取出来放到内存里,这样当需要获取数据时,就能够直接从内存中拿到数据返回,能够很大程度的提高速度。但是一般redis是单独部署成集群,所以会有网络IO上的消耗,虽然与redis集群的链接已经有连接池这种工具,但是数据传输上也还是会有一定消耗。所以就有了应用内缓存,如:caffeine。当应用内缓存有符合条件的数据时,就可以直接使用,而不用通过网络到redis中去获取,这样就形成了两级缓存。应用内缓存叫做一级缓存,远程缓存(如redis)叫做二级缓存

spring cache


当使用缓存的时候,一般是如下的流程:

spring boot + spring cache 实现两级缓存(redis + caffeine)

从流程图中可以看出,为了使用缓存,在原有业务处理的基础上,增加了很多对于缓存的操作,如果将这些耦合到业务代码当中,开发起来就有很多重复性的工作,并且不太利于根据代码去理解业务。

spring cache是spring-context包中提供的基于注解方式使用的缓存组件,定义了一些标准接口,通过实现这些接口,就可以通过在方法上增加注解来实现缓存。这样就能够避免缓存代码与业务处理耦合在一起的问题。spring cache的实现是使用spring aop中对方法切面(MethodInterceptor)封装的扩展,当然spring aop也是基于Aspect来实现的。

spring cache核心的接口就两个:Cache和CacheManager

spring boot + spring cache 实现两级缓存(redis + caffeine)

Cache接口

提供缓存的具体操作,比如缓存的放入、读取、清理,spring框架中默认提供的实现有:

spring boot + spring cache 实现两级缓存(redis + caffeine)

除了RedisCache是在spring-data-redis包中,其他的基本都是在spring-context-support包中

spring boot + spring cache 实现两级缓存(redis + caffeine)


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
1#Cache.java
2
3package org.springframework.cache;
4
5import java.util.concurrent.Callable;
6
7public interface Cache {
8
9   // cacheName,缓存的名字,默认实现中一般是CacheManager创建Cache的bean时传入cacheName
10  String getName();
11
12  // 获取实际使用的缓存,如:RedisTemplate、com.github.benmanes.caffeine.cache.Cache<Object, Object>。暂时没发现实际用处,可能只是提供获取原生缓存的bean,以便需要扩展一些缓存操作或统计之类的东西
13  Object getNativeCache();
14
15  // 通过key获取缓存值,注意返回的是ValueWrapper,为了兼容存储空值的情况,将返回值包装了一层,通过get方法获取实际值
16  ValueWrapper get(Object key);
17
18  // 通过key获取缓存值,返回的是实际值,即方法的返回值类型
19  <T> T get(Object key, Class<T> type);
20
21  // 通过key获取缓存值,可以使用valueLoader.call()来调使用@Cacheable注解的方法。当@Cacheable注解的sync属性配置为true时使用此方法。因此方法内需要保证回源到数据库的同步性。避免在缓存失效时大量请求回源到数据库
22  <T> T get(Object key, Callable<T> valueLoader);
23
24  // 将@Cacheable注解方法返回的数据放入缓存中
25  void put(Object key, Object value);
26
27  // 当缓存中不存在key时才放入缓存。返回值是当key存在时原有的数据
28  ValueWrapper putIfAbsent(Object key, Object value);
29
30  // 删除缓存
31  void evict(Object key);
32
33  // 删除缓存中的所有数据。需要注意的是,具体实现中只删除使用@Cacheable注解缓存的所有数据,不要影响应用内的其他缓存
34  void clear();
35
36  // 缓存返回值的包装
37  interface ValueWrapper {
38
39      // 返回实际缓存的对象
40      Object get();
41  }
42
43  // 当{@link #get(Object, Callable)}抛出异常时,会包装成此异常抛出
44  @SuppressWarnings("serial")
45  class ValueRetrievalException extends RuntimeException {
46
47      private final Object key;
48
49      public ValueRetrievalException(Object key, Callable<?> loader, Throwable ex) {
50          super(String.format("Value for key '%s' could not be loaded using '%s'", key, loader), ex);
51          this.key = key;
52      }
53
54      public Object getKey() {
55          return this.key;
56      }
57  }
58}
59
60

CacheManager接口

主要提供Cache实现bean的创建,每个应用里可以通过cacheName来对Cache进行隔离,每个cacheName对应一个Cache实现。spring框架中默认提供的实现与Cache的实现都是成对出现,包结构也在上图中


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1#CacheManager.java
2
3package org.springframework.cache;
4
5import java.util.Collection;
6
7public interface CacheManager {
8
9   // 通过cacheName创建Cache的实现bean,具体实现中需要存储已创建的Cache实现bean,避免重复创建,也避免内存缓存对象(如Caffeine)重新创建后原来缓存内容丢失的情况
10  Cache getCache(String name);
11
12  // 返回所有的cacheName
13  Collection<String> getCacheNames();
14}
15
16

常用注解说明

  • @Cacheable:主要应用到查询数据的方法上


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
1package org.springframework.cache.annotation;
2
3import java.lang.annotation.Documented;
4import java.lang.annotation.ElementType;
5import java.lang.annotation.Inherited;
6import java.lang.annotation.Retention;
7import java.lang.annotation.RetentionPolicy;
8import java.lang.annotation.Target;
9import java.util.concurrent.Callable;
10
11import org.springframework.core.annotation.AliasFor;
12
13@Target({ElementType.METHOD, ElementType.TYPE})
14@Retention(RetentionPolicy.RUNTIME)
15@Inherited
16@Documented
17public @interface Cacheable {
18
19        // cacheNames,CacheManager就是通过这个名称创建对应的Cache实现bean
20  @AliasFor("cacheNames")
21  String[] value() default {};
22
23  @AliasFor("value")
24  String[] cacheNames() default {};
25
26        // 缓存的key,支持SpEL表达式。默认是使用所有参数及其计算的hashCode包装后的对象(SimpleKey)
27  String key() default "";
28
29  // 缓存key生成器,默认实现是SimpleKeyGenerator
30  String keyGenerator() default "";
31
32  // 指定使用哪个CacheManager
33  String cacheManager() default "";
34
35  // 缓存解析器
36  String cacheResolver() default "";
37
38  // 缓存的条件,支持SpEL表达式,当达到满足的条件时才缓存数据。在调用方法前后都会判断
39  String condition() default "";
40        
41        // 满足条件时不更新缓存,支持SpEL表达式,只在调用方法后判断
42  String unless() default "";
43
44  // 回源到实际方法获取数据时,是否要保持同步,如果为false,调用的是Cache.get(key)方法;如果为true,调用的是Cache.get(key, Callable)方法
45  boolean sync() default false;
46
47}
48
49
  • @CacheEvict:清除缓存,主要应用到删除数据的方法上。相比Cacheable多了两个属性


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
1package org.springframework.cache.annotation;
2
3import java.lang.annotation.Documented;
4import java.lang.annotation.ElementType;
5import java.lang.annotation.Inherited;
6import java.lang.annotation.Retention;
7import java.lang.annotation.RetentionPolicy;
8import java.lang.annotation.Target;
9
10import org.springframework.core.annotation.AliasFor;
11
12@Target({ElementType.METHOD, ElementType.TYPE})
13@Retention(RetentionPolicy.RUNTIME)
14@Inherited
15@Documented
16public @interface CacheEvict {
17
18        // ...相同属性说明请参考@Cacheable中的说明
19
20  // 是否要清除所有缓存的数据,为false时调用的是Cache.evict(key)方法;为true时调用的是Cache.clear()方法
21  boolean allEntries() default false;
22
23  // 调用方法之前或之后清除缓存
24  boolean beforeInvocation() default false;
25}
26
27
  • @CachePut:放入缓存,主要用到对数据有更新的方法上。属性说明参考@Cacheable

  • @Caching:用于在一个方法上配置多种注解

  • @EnableCaching:启用spring cache缓存,作为总的开关,在spring boot的启动类或配置类上需要加上此注解才会生效

spring boot + spring cache


spring boot中已经整合了spring cache,并且提供了多种缓存的配置,在使用时只需要配置使用哪个缓存(enum CacheType)即可。

spring boot + spring cache 实现两级缓存(redis + caffeine)

spring boot中多增加了一个可以扩展的东西,就是CacheManagerCustomizer接口,可以自定义实现这个接口,然后对CacheManager做一些设置,比如:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
1package com.itopener.demo.cache.redis.config;
2
3import java.util.Map;
4import java.util.concurrent.ConcurrentHashMap;
5
6import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizer;
7import org.springframework.data.redis.cache.RedisCacheManager;
8
9public class RedisCacheManagerCustomizer implements CacheManagerCustomizer<RedisCacheManager> {
10
11  @Override
12  public void customize(RedisCacheManager cacheManager) {
13      // 默认过期时间,单位秒
14      cacheManager.setDefaultExpiration(1000);
15      cacheManager.setUsePrefix(false);
16      Map<String, Long> expires = new ConcurrentHashMap<String, Long>();
17      expires.put("userIdCache", 2000L);
18      cacheManager.setExpires(expires);
19  }
20
21}
22
23

加载这个bean:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1package com.itopener.demo.cache.redis.config;
2
3import org.springframework.context.annotation.Bean;
4import org.springframework.context.annotation.Configuration;
5
6/**
7 * @author fuwei.deng
8 * @date 2017年12月22日 上午10:24:54
9 * @version 1.0.0
10 */
11@Configuration
12public class CacheRedisConfiguration {
13 
14  @Bean
15  public RedisCacheManagerCustomizer redisCacheManagerCustomizer() {
16      return new RedisCacheManagerCustomizer();
17  }
18}
19
20

常用的缓存就是Redis了,Redis对于spring cache接口的实现是在spring-data-redis包中

spring boot + spring cache 实现两级缓存(redis + caffeine)

这里提下我认为的RedisCache实现中的缺陷:

1.在缓存失效的瞬间,如果有线程获取缓存数据,可能出现返回null的情况,原因是RedisCache实现中是如下步骤:

  • 判断缓存key是否存在
  • 如果key存在,再获取缓存数据,并返回

因此当判断key存在后缓存失效了,再去获取缓存是没有数据的,就返回null了。

2.RedisCacheManager中是否允许存储空值的属性(cacheNullValues)默认为false,即不允许存储空值,这样会存在缓存穿透的风险。缺陷是这个属性是final类型的,只能在创建对象是通过构造方法传入,所以要避免缓存穿透就只能自己在应用内声明RedisCacheManager这个bean了

3.RedisCacheManager中的属性无法通过配置文件直接配置,只能在应用内实现CacheManagerCustomizer接口来进行设置,个人认为不太方便

Caffeine


Caffeine是一个基于Google开源的Guava设计理念的一个高性能内存缓存,使用java8开发,spring boot引入Caffeine后已经逐步废弃Guava的整合了。Caffeine源码及介绍地址:caffeine

caffeine提供了多种缓存填充策略、值回收策略,同时也包含了缓存命中次数等统计数据,对缓存的优化能够提供很大帮助

caffeine的介绍可以参考:http://www.cnblogs.com/oopsguy/p/7731659.html

这里简单说下caffeine基于时间的回收策略有以下几种:

  • expireAfterAccess:访问后到期,从上次读或写发生后的过期时间
  • expireAfterWrite:写入后到期,从上次写入发生之后的过期时间
  • 自定义策略:到期时间由实现Expiry接口后单独计算

spring boot + spring cache 实现两级缓存(redis + caffeine)


本人开头提到了,就算是使用了redis缓存,也会存在一定程度的网络传输上的消耗,在实际应用当中,会存在一些变更频率非常低的数据,就可以直接缓存在应用内部,对于一些实时性要求不太高的数据,也可以在应用内部缓存一定时间,减少对redis的访问,提高响应速度

由于spring-data-redis框架中redis对spring cache的实现有一些不足,在使用起来可能会出现一些问题,所以就不基于原来的实现去扩展了,直接参考实现方式,去实现Cache和CacheManager接口

还需要注意一点,一般应用都部署了多个节点,一级缓存是在应用内的缓存,所以当对数据更新和清除时,需要通知所有节点进行清理缓存的操作。可以有多种方式来实现这种效果,比如:zookeeper、MQ等,但是既然用了redis缓存,redis本身是有支持订阅/发布功能的,所以就不依赖其他组件了,直接使用redis的通道来通知其他节点进行清理缓存的操作

以下就是对spring boot + spring cache实现两级缓存(redis + caffeine)的starter封装步骤和源码

  • 定义properties配置属性类


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
175
176
177
178
179
180
181
1package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure;
2
3import java.util.HashMap;
4import java.util.HashSet;
5import java.util.Map;
6import java.util.Set;
7
8import org.springframework.boot.context.properties.ConfigurationProperties;
9
10/**  
11 * @author fuwei.deng
12 * @date 2018年1月29日 上午11:32:15
13 * @version 1.0.0
14 */
15@ConfigurationProperties(prefix = "spring.cache.multi")
16public class CacheRedisCaffeineProperties {
17 
18  private Set<String> cacheNames = new HashSet<>();
19 
20  /** 是否存储空值,默认true,防止缓存穿透*/
21  private boolean cacheNullValues = true;
22 
23  /** 是否动态根据cacheName创建Cache的实现,默认true*/
24  private boolean dynamic = true;
25 
26  /** 缓存key的前缀*/
27  private String cachePrefix;
28 
29  private Redis redis = new Redis();
30 
31  private Caffeine caffeine = new Caffeine();
32
33  public class Redis {
34     
35      /** 全局过期时间,单位毫秒,默认不过期*/
36      private long defaultExpiration = 0;
37     
38      /** 每个cacheName的过期时间,单位毫秒,优先级比defaultExpiration高*/
39      private Map<String, Long> expires = new HashMap<>();
40     
41      /** 缓存更新时通知其他节点的topic名称*/
42      private String topic = "cache:redis:caffeine:topic";
43
44      public long getDefaultExpiration() {
45          return defaultExpiration;
46      }
47
48      public void setDefaultExpiration(long defaultExpiration) {
49          this.defaultExpiration = defaultExpiration;
50      }
51
52      public Map<String, Long> getExpires() {
53          return expires;
54      }
55
56      public void setExpires(Map<String, Long> expires) {
57          this.expires = expires;
58      }
59
60      public String getTopic() {
61          return topic;
62      }
63
64      public void setTopic(String topic) {
65          this.topic = topic;
66      }
67     
68  }
69 
70  public class Caffeine {
71     
72      /** 访问后过期时间,单位毫秒*/
73      private long expireAfterAccess;
74     
75      /** 写入后过期时间,单位毫秒*/
76      private long expireAfterWrite;
77     
78      /** 写入后刷新时间,单位毫秒*/
79      private long refreshAfterWrite;
80     
81      /** 初始化大小*/
82      private int initialCapacity;
83     
84      /** 最大缓存对象个数,超过此数量时之前放入的缓存将失效*/
85      private long maximumSize;
86     
87      /** 由于权重需要缓存对象来提供,对于使用spring cache这种场景不是很适合,所以暂不支持配置*/
88//        private long maximumWeight;
89     
90      public long getExpireAfterAccess() {
91          return expireAfterAccess;
92      }
93
94      public void setExpireAfterAccess(long expireAfterAccess) {
95          this.expireAfterAccess = expireAfterAccess;
96      }
97
98      public long getExpireAfterWrite() {
99          return expireAfterWrite;
100     }
101
102     public void setExpireAfterWrite(long expireAfterWrite) {
103         this.expireAfterWrite = expireAfterWrite;
104     }
105
106     public long getRefreshAfterWrite() {
107         return refreshAfterWrite;
108     }
109
110     public void setRefreshAfterWrite(long refreshAfterWrite) {
111         this.refreshAfterWrite = refreshAfterWrite;
112     }
113
114     public int getInitialCapacity() {
115         return initialCapacity;
116     }
117
118     public void setInitialCapacity(int initialCapacity) {
119         this.initialCapacity = initialCapacity;
120     }
121
122     public long getMaximumSize() {
123         return maximumSize;
124     }
125
126     public void setMaximumSize(long maximumSize) {
127         this.maximumSize = maximumSize;
128     }
129 }
130
131 public Set<String> getCacheNames() {
132     return cacheNames;
133 }
134
135 public void setCacheNames(Set<String> cacheNames) {
136     this.cacheNames = cacheNames;
137 }
138
139 public boolean isCacheNullValues() {
140     return cacheNullValues;
141 }
142
143 public void setCacheNullValues(boolean cacheNullValues) {
144     this.cacheNullValues = cacheNullValues;
145 }
146
147 public boolean isDynamic() {
148     return dynamic;
149 }
150
151 public void setDynamic(boolean dynamic) {
152     this.dynamic = dynamic;
153 }
154
155 public String getCachePrefix() {
156     return cachePrefix;
157 }
158
159 public void setCachePrefix(String cachePrefix) {
160     this.cachePrefix = cachePrefix;
161 }
162
163 public Redis getRedis() {
164     return redis;
165 }
166
167 public void setRedis(Redis redis) {
168     this.redis = redis;
169 }
170
171 public Caffeine getCaffeine() {
172     return caffeine;
173 }
174
175 public void setCaffeine(Caffeine caffeine) {
176     this.caffeine = caffeine;
177 }
178
179}
180
181
  • spring cache中有实现Cache接口的一个抽象类AbstractValueAdaptingCache,包含了空值的包装和缓存值的包装,所以就不用实现Cache接口了,直接实现AbstractValueAdaptingCache抽象类


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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
1package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support;
2
3import java.lang.reflect.Constructor;
4import java.util.Map;
5import java.util.Set;
6import java.util.concurrent.Callable;
7import java.util.concurrent.TimeUnit;
8import java.util.concurrent.locks.ReentrantLock;
9
10import org.slf4j.Logger;
11import org.slf4j.LoggerFactory;
12import org.springframework.cache.support.AbstractValueAdaptingCache;
13import org.springframework.data.redis.core.RedisTemplate;
14import org.springframework.util.StringUtils;
15
16import com.github.benmanes.caffeine.cache.Cache;
17import com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.CacheRedisCaffeineProperties;
18
19/**
20 * @author fuwei.deng
21 * @date 2018年1月26日 下午5:24:11
22 * @version 1.0.0
23 */
24public class RedisCaffeineCache extends AbstractValueAdaptingCache {
25 
26  private final Logger logger = LoggerFactory.getLogger(RedisCaffeineCache.class);
27
28  private String name;
29 
30  private RedisTemplate<Object, Object> redisTemplate;
31
32  private Cache<Object, Object> caffeineCache;
33
34  private String cachePrefix;
35
36  private long defaultExpiration = 0;
37
38  private Map<String, Long> expires;
39 
40  private String topic = "cache:redis:caffeine:topic";
41 
42  protected RedisCaffeineCache(boolean allowNullValues) {
43      super(allowNullValues);
44  }
45 
46  public RedisCaffeineCache(String name, RedisTemplate<Object, Object> redisTemplate, Cache<Object, Object> caffeineCache, CacheRedisCaffeineProperties cacheRedisCaffeineProperties) {
47      super(cacheRedisCaffeineProperties.isCacheNullValues());
48      this.name = name;
49      this.redisTemplate = redisTemplate;
50      this.caffeineCache = caffeineCache;
51      this.cachePrefix = cacheRedisCaffeineProperties.getCachePrefix();
52      this.defaultExpiration = cacheRedisCaffeineProperties.getRedis().getDefaultExpiration();
53      this.expires = cacheRedisCaffeineProperties.getRedis().getExpires();
54      this.topic = cacheRedisCaffeineProperties.getRedis().getTopic();
55  }
56
57  @Override
58  public String getName() {
59      return this.name;
60  }
61
62  @Override
63  public Object getNativeCache() {
64      return this;
65  }
66
67  @SuppressWarnings("unchecked")
68  @Override
69  public <T> T get(Object key, Callable<T> valueLoader) {
70      Object value = lookup(key);
71      if(value != null) {
72          return (T) value;
73      }
74     
75      ReentrantLock lock = new ReentrantLock();
76      try {
77          lock.lock();
78          value = lookup(key);
79          if(value != null) {
80              return (T) value;
81          }
82          value = valueLoader.call();
83          Object storeValue = toStoreValue(valueLoader.call());
84          put(key, storeValue);
85          return (T) value;
86      } catch (Exception e) {
87          try {
88                Class<?> c = Class.forName("org.springframework.cache.Cache$ValueRetrievalException");
89                Constructor<?> constructor = c.getConstructor(Object.class, Callable.class, Throwable.class);
90                RuntimeException exception = (RuntimeException) constructor.newInstance(key, valueLoader, e.getCause());
91                throw exception;                
92            } catch (Exception e1) {
93                throw new IllegalStateException(e1);
94            }
95      } finally {
96          lock.unlock();
97      }
98  }
99
100 @Override
101 public void put(Object key, Object value) {
102     if (!super.isAllowNullValues() && value == null) {
103         this.evict(key);
104            return;
105        }
106     long expire = getExpire();
107     if(expire > 0) {
108         redisTemplate.opsForValue().set(getKey(key), toStoreValue(value), expire, TimeUnit.MILLISECONDS);
109     } else {
110         redisTemplate.opsForValue().set(getKey(key), toStoreValue(value));
111     }
112    
113     push(new CacheMessage(this.name, key));
114    
115     caffeineCache.put(key, value);
116 }
117
118 @Override
119 public ValueWrapper putIfAbsent(Object key, Object value) {
120     Object cacheKey = getKey(key);
121     Object prevValue = null;
122     // 考虑使用分布式锁,或者将redis的setIfAbsent改为原子性操作
123     synchronized (key) {
124         prevValue = redisTemplate.opsForValue().get(cacheKey);
125         if(prevValue == null) {
126             long expire = getExpire();
127             if(expire > 0) {
128                 redisTemplate.opsForValue().set(getKey(key), toStoreValue(value), expire, TimeUnit.MILLISECONDS);
129             } else {
130                 redisTemplate.opsForValue().set(getKey(key), toStoreValue(value));
131             }
132            
133             push(new CacheMessage(this.name, key));
134            
135             caffeineCache.put(key, toStoreValue(value));
136         }
137     }
138     return toValueWrapper(prevValue);
139 }
140
141 @Override
142 public void evict(Object key) {
143     // 先清除redis中缓存数据,然后清除caffeine中的缓存,避免短时间内如果先清除caffeine缓存后其他请求会再从redis里加载到caffeine中
144     redisTemplate.delete(getKey(key));
145    
146     push(new CacheMessage(this.name, key));
147    
148     caffeineCache.invalidate(key);
149 }
150
151 @Override
152 public void clear() {
153     // 先清除redis中缓存数据,然后清除caffeine中的缓存,避免短时间内如果先清除caffeine缓存后其他请求会再从redis里加载到caffeine中
154     Set<Object> keys = redisTemplate.keys(this.name.concat(":"));
155     for(Object key : keys) {
156         redisTemplate.delete(key);
157     }
158    
159     push(new CacheMessage(this.name, null));
160    
161     caffeineCache.invalidateAll();
162 }
163
164 @Override
165 protected Object lookup(Object key) {
166     Object cacheKey = getKey(key);
167     Object value = caffeineCache.getIfPresent(key);
168     if(value != null) {
169         logger.debug("get cache from caffeine, the key is : {}", cacheKey);
170         return value;
171     }
172    
173     value = redisTemplate.opsForValue().get(cacheKey);
174    
175     if(value != null) {
176         logger.debug("get cache from redis and put in caffeine, the key is : {}", cacheKey);
177         caffeineCache.put(key, value);
178     }
179     return value;
180 }
181
182 private Object getKey(Object key) {
183     return this.name.concat(":").concat(StringUtils.isEmpty(cachePrefix) ? key.toString() : cachePrefix.concat(":").concat(key.toString()));
184 }
185
186 private long getExpire() {
187     long expire = defaultExpiration;
188     Long cacheNameExpire = expires.get(this.name);
189     return cacheNameExpire == null ? expire : cacheNameExpire.longValue();
190 }
191
192 /**
193  * @description 缓存变更时通知其他节点清理本地缓存
194  * @author fuwei.deng
195  * @date 2018年1月31日 下午3:20:28
196  * @version 1.0.0
197  * @param message
198  */
199 private void push(CacheMessage message) {
200     redisTemplate.convertAndSend(topic, message);
201 }
202
203 /**
204  * @description 清理本地缓存
205  * @author fuwei.deng
206  * @date 2018年1月31日 下午3:15:39
207  * @version 1.0.0
208  * @param key
209  */
210 public void clearLocal(Object key) {
211     logger.debug("clear local cache, the key is : {}", key);
212     if(key == null) {
213         caffeineCache.invalidateAll();
214     } else {
215         caffeineCache.invalidate(key);
216     }
217 }
218}
219
220
  • 实现CacheManager接口


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
1package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support;
2
3import java.util.Collection;
4import java.util.Set;
5import java.util.concurrent.ConcurrentHashMap;
6import java.util.concurrent.ConcurrentMap;
7import java.util.concurrent.TimeUnit;
8
9import org.slf4j.Logger;
10import org.slf4j.LoggerFactory;
11import org.springframework.cache.Cache;
12import org.springframework.cache.CacheManager;
13import org.springframework.data.redis.core.RedisTemplate;
14
15import com.github.benmanes.caffeine.cache.Caffeine;
16import com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.CacheRedisCaffeineProperties;
17
18/**
19 * @author fuwei.deng
20 * @date 2018年1月26日 下午5:24:52
21 * @version 1.0.0
22 */
23public class RedisCaffeineCacheManager implements CacheManager {
24 
25  private final Logger logger = LoggerFactory.getLogger(RedisCaffeineCacheManager.class);
26 
27  private ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<String, Cache>();
28 
29  private CacheRedisCaffeineProperties cacheRedisCaffeineProperties;
30 
31  private RedisTemplate<Object, Object> redisTemplate;
32
33  private boolean dynamic = true;
34
35  private Set<String> cacheNames;
36
37  public RedisCaffeineCacheManager(CacheRedisCaffeineProperties cacheRedisCaffeineProperties,
38          RedisTemplate<Object, Object> redisTemplate) {
39      super();
40      this.cacheRedisCaffeineProperties = cacheRedisCaffeineProperties;
41      this.redisTemplate = redisTemplate;
42      this.dynamic = cacheRedisCaffeineProperties.isDynamic();
43      this.cacheNames = cacheRedisCaffeineProperties.getCacheNames();
44  }
45
46  @Override
47  public Cache getCache(String name) {
48      Cache cache = cacheMap.get(name);
49      if(cache != null) {
50          return cache;
51      }
52      if(!dynamic && !cacheNames.contains(name)) {
53          return cache;
54      }
55     
56      cache = new RedisCaffeineCache(name, redisTemplate, caffeineCache(), cacheRedisCaffeineProperties);
57      Cache oldCache = cacheMap.putIfAbsent(name, cache);
58      logger.debug("create cache instance, the cache name is : {}", name);
59      return oldCache == null ? cache : oldCache;
60  }
61 
62  public com.github.benmanes.caffeine.cache.Cache<Object, Object> caffeineCache(){
63      Caffeine<Object, Object> cacheBuilder = Caffeine.newBuilder();
64      if(cacheRedisCaffeineProperties.getCaffeine().getExpireAfterAccess() > 0) {
65          cacheBuilder.expireAfterAccess(cacheRedisCaffeineProperties.getCaffeine().getExpireAfterAccess(), TimeUnit.MILLISECONDS);
66      }
67      if(cacheRedisCaffeineProperties.getCaffeine().getExpireAfterWrite() > 0) {
68          cacheBuilder.expireAfterWrite(cacheRedisCaffeineProperties.getCaffeine().getExpireAfterWrite(), TimeUnit.MILLISECONDS);
69      }
70      if(cacheRedisCaffeineProperties.getCaffeine().getInitialCapacity() > 0) {
71          cacheBuilder.initialCapacity(cacheRedisCaffeineProperties.getCaffeine().getInitialCapacity());
72      }
73      if(cacheRedisCaffeineProperties.getCaffeine().getMaximumSize() > 0) {
74          cacheBuilder.maximumSize(cacheRedisCaffeineProperties.getCaffeine().getMaximumSize());
75      }
76      if(cacheRedisCaffeineProperties.getCaffeine().getRefreshAfterWrite() > 0) {
77          cacheBuilder.refreshAfterWrite(cacheRedisCaffeineProperties.getCaffeine().getRefreshAfterWrite(), TimeUnit.MILLISECONDS);
78      }
79      return cacheBuilder.build();
80  }
81
82  @Override
83  public Collection<String> getCacheNames() {
84      return this.cacheNames;
85  }
86 
87  public void clearLocal(String cacheName, Object key) {
88      Cache cache = cacheMap.get(cacheName);
89      if(cache == null) {
90          return ;
91      }
92     
93      RedisCaffeineCache redisCaffeineCache = (RedisCaffeineCache) cache;
94      redisCaffeineCache.clearLocal(key);
95  }
96}
97
98
  • 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
35
36
37
38
39
40
41
42
43
1package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support;
2
3import java.io.Serializable;
4
5/**  
6 * @author fuwei.deng
7 * @date 2018年1月29日 下午1:31:17
8 * @version 1.0.0
9 */
10public class CacheMessage implements Serializable {
11
12  /** */
13  private static final long serialVersionUID = 5987219310442078193L;
14
15  private String cacheName;
16 
17  private Object key;
18
19  public CacheMessage(String cacheName, Object key) {
20      super();
21      this.cacheName = cacheName;
22      this.key = key;
23  }
24
25  public String getCacheName() {
26      return cacheName;
27  }
28
29  public void setCacheName(String cacheName) {
30      this.cacheName = cacheName;
31  }
32
33  public Object getKey() {
34      return key;
35  }
36
37  public void setKey(Object key) {
38      this.key = key;
39  }
40 
41}
42
43
  • 监听redis消息需要实现MessageListener接口


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
1package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support;
2
3import org.slf4j.Logger;
4import org.slf4j.LoggerFactory;
5import org.springframework.data.redis.connection.Message;
6import org.springframework.data.redis.connection.MessageListener;
7import org.springframework.data.redis.core.RedisTemplate;
8
9/**  
10 * @author fuwei.deng
11 * @date 2018年1月30日 下午5:22:33
12 * @version 1.0.0
13 */
14public class CacheMessageListener implements MessageListener {
15 
16  private final Logger logger = LoggerFactory.getLogger(CacheMessageListener.class);
17
18  private RedisTemplate<Object, Object> redisTemplate;
19 
20  private RedisCaffeineCacheManager redisCaffeineCacheManager;
21
22  public CacheMessageListener(RedisTemplate<Object, Object> redisTemplate,
23          RedisCaffeineCacheManager redisCaffeineCacheManager) {
24      super();
25      this.redisTemplate = redisTemplate;
26      this.redisCaffeineCacheManager = redisCaffeineCacheManager;
27  }
28
29  @Override
30  public void onMessage(Message message, byte[] pattern) {
31      CacheMessage cacheMessage = (CacheMessage) redisTemplate.getValueSerializer().deserialize(message.getBody());
32      logger.debug("recevice a redis topic message, clear local cache, the cacheName is {}, the key is {}", cacheMessage.getCacheName(), cacheMessage.getKey());
33      redisCaffeineCacheManager.clearLocal(cacheMessage.getCacheName(), cacheMessage.getKey());
34  }
35
36}
37
38
  • 增加spring boot配置类


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
1package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure;
2
3import org.springframework.beans.factory.annotation.Autowired;
4import org.springframework.boot.autoconfigure.AutoConfigureAfter;
5import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
6import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
7import org.springframework.boot.context.properties.EnableConfigurationProperties;
8import org.springframework.context.annotation.Bean;
9import org.springframework.context.annotation.Configuration;
10import org.springframework.data.redis.core.RedisTemplate;
11import org.springframework.data.redis.listener.ChannelTopic;
12import org.springframework.data.redis.listener.RedisMessageListenerContainer;
13
14import com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support.CacheMessageListener;
15import com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support.RedisCaffeineCacheManager;
16
17/**  
18 * @author fuwei.deng
19 * @date 2018年1月26日 下午5:23:03
20 * @version 1.0.0
21 */
22@Configuration
23@AutoConfigureAfter(RedisAutoConfiguration.class)
24@EnableConfigurationProperties(CacheRedisCaffeineProperties.class)
25public class CacheRedisCaffeineAutoConfiguration {
26 
27  @Autowired
28  private CacheRedisCaffeineProperties cacheRedisCaffeineProperties;
29 
30  @Bean
31  @ConditionalOnBean(RedisTemplate.class)
32  public RedisCaffeineCacheManager cacheManager(RedisTemplate<Object, Object> redisTemplate) {
33      return new RedisCaffeineCacheManager(cacheRedisCaffeineProperties, redisTemplate);
34  }
35 
36  @Bean
37  public RedisMessageListenerContainer redisMessageListenerContainer(RedisTemplate<Object, Object> redisTemplate,
38          RedisCaffeineCacheManager redisCaffeineCacheManager) {
39      RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
40      redisMessageListenerContainer.setConnectionFactory(redisTemplate.getConnectionFactory());
41      CacheMessageListener cacheMessageListener = new CacheMessageListener(redisTemplate, redisCaffeineCacheManager);
42      redisMessageListenerContainer.addMessageListener(cacheMessageListener, new ChannelTopic(cacheRedisCaffeineProperties.getRedis().getTopic()));
43      return redisMessageListenerContainer;
44  }
45}
46
47
  • 在resources/META-INF/spring.factories文件中增加spring boot配置扫描


1
2
3
4
5
1# Auto Configure
2org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
3com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.CacheRedisCaffeineAutoConfiguration
4
5
  • 接下来就可以使用maven引入使用了


1
2
3
4
5
6
7
8
1<dependency>
2    <groupId>com.itopener</groupId>
3    <artifactId>cache-redis-caffeine-spring-boot-starter</artifactId>
4    <version>1.0.0-SNAPSHOT</version>
5    <type>pom</type>
6</dependency>
7
8
  • 在启动类上增加@EnableCaching注解,在需要缓存的方法上增加@Cacheable注解


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
1package com.itopener.demo.cache.redis.caffeine.service;
2
3import java.util.Random;
4
5import org.slf4j.Logger;
6import org.slf4j.LoggerFactory;
7import org.springframework.cache.annotation.CacheEvict;
8import org.springframework.cache.annotation.CachePut;
9import org.springframework.cache.annotation.Cacheable;
10import org.springframework.stereotype.Service;
11
12import com.itopener.demo.cache.redis.caffeine.vo.UserVO;
13import com.itopener.utils.TimestampUtil;
14
15@Service
16public class CacheRedisCaffeineService {
17 
18  private final Logger logger = LoggerFactory.getLogger(CacheRedisCaffeineService.class);
19
20  @Cacheable(key = "'cache_user_id_' + #id", value = "userIdCache", cacheManager = "cacheManager")
21  public UserVO get(long id) {
22      logger.info("get by id from db");
23      UserVO user = new UserVO();
24      user.setId(id);
25      user.setName("name" + id);
26      user.setCreateTime(TimestampUtil.current());
27      return user;
28  }
29 
30  @Cacheable(key = "'cache_user_name_' + #name", value = "userNameCache", cacheManager = "cacheManager")
31  public UserVO get(String name) {
32      logger.info("get by name from db");
33      UserVO user = new UserVO();
34      user.setId(new Random().nextLong());
35      user.setName(name);
36      user.setCreateTime(TimestampUtil.current());
37      return user;
38  }
39 
40  @CachePut(key = "'cache_user_id_' + #userVO.id", value = "userIdCache", cacheManager = "cacheManager")
41  public UserVO update(UserVO userVO) {
42      logger.info("update to db");
43      userVO.setCreateTime(TimestampUtil.current());
44      return userVO;
45  }
46 
47  @CacheEvict(key = "'cache_user_id_' + #id", value = "userIdCache", cacheManager = "cacheManager")
48  public void delete(long id) {
49      logger.info("delete from db");
50  }
51}
52
53
  • properties文件中redis的配置跟使用redis是一样的,可以增加两级缓存的配置


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1#两级缓存的配置
2spring.cache.multi.caffeine.expireAfterAccess=5000
3spring.cache.multi.redis.defaultExpiration=60000
4
5#spring cache配置
6spring.cache.cache-names=userIdCache,userNameCache
7
8#redis配置
9#spring.redis.timeout=10000
10#spring.redis.password=redispwd
11#redis pool
12#spring.redis.pool.maxIdle=10
13#spring.redis.pool.minIdle=2
14#spring.redis.pool.maxActive=10
15#spring.redis.pool.maxWait=3000
16#redis cluster
17spring.redis.cluster.nodes=127.0.0.1:7001,127.0.0.1:7002,127.0.0.1:7003,127.0.0.1:7004,127.0.0.1:7005,127.0.0.1:7006
18spring.redis.cluster.maxRedirects=3
19
20

扩展


  • 个人认为redisson的封装更方便一些

  • 对于spring cache缓存的实现没有那么多的缺陷

    • 使用redis的HASH结构,可以针对不同的hashKey设置过期时间,清理的时候会更方便
    • 如果基于redisson来实现多级缓存,可以继承RedissonCache,在对应方法增加一级缓存的操作即可
    • 如果有使用分布式锁的情况就更方便了,可以直接使用Redisson中封装的分布式锁
    • redisson中的发布订阅封装得更好用
  • 后续可以增加对于缓存命中率的统计endpoint,这样就可以更好的监控各个缓存的命中情况,以便对缓存配置进行优化

源码

https://gitee.com/itopener/springboot

给TA打赏
共{{data.count}}人
人已打赏
安全技术

详解Node.js API系列C/C++ Addons(1) API文档

2021-12-21 16:36:11

安全技术

从零搭建自己的SpringBoot后台框架(二十三)

2022-1-12 12:36:11

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索