spring-Cache

发现项目里面的redis缓存与数据库的数据混乱不一致,因为很多自定义的数据库update方法更新了数据库,但是并没有更新redis,于是想在底层实现自动缓存

Spring cache简单使用

教程

  1. 引入依赖

    1
    2
    compile group: 'org.springframework.boot', name: 'spring-boot-starter-cache', version: '2.1.1.RELEASE'
    compile group: 'org.springframework.boot', name: 'spring-boot-starter-data-redis', version: '2.1.1.RELEASE'
  2. 添加redis缓存的中间件,缓存的中间件也可以不用redis用其他中间件一样的,可选generic,ehcache,hazelcast,infinispan,jcache,redis,guava,simple,none

    1
    2
    spring.redis.host=gt163.cn
    spring.redis.port=14043
  3. 开启cache功能,在@SpringBootApplication启动类或@Configuration配置类上面添加该注解@EnableCaching

  4. 使用缓存功能,在要缓存的方法上面或者类上面添加注解@Cacheable("<redis里面的唯一key,也可以叫表名>")

    1
    2
    3
    4
    5
    //例如
    @Cacheable("user_info")
    public User findById(String id) {
    return userDao.findById(id);
    }

    cYVwvT.png](https://imgtu.com/i/cJAnB9)

常见几个注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//开启缓存功能
@EnableCaching
//缓存没有数据查执行方法里面的内容,然后将执行的结果缓存起来,缓存里面有直接读缓存,不会执行方法里面的内容,参数会作为key
@Cacheable("user_info")
public User findById(String id)
//参数unless对结果进行判断,condition对参数进行判断
//缓存不管是否存在都会执行方法里面的内容并更新缓存
@CachePut(value="user_info")
//删除缓存
@CacheEvict(value="user_info")
//多个缓存分组
@Caching
//注解到类上面,类里面的方法只需要添加注解@Cacheable,不用在指定cacheName了
@CacheConfig(cacheNames={"user_info"})

redis缓存mongo数据库表的架构设计

设计方案一

详细代码见github:iexxk/springLeaning:mongo

BaseDao接口层添加缓存注解,然后在各个子类继承实现,达到通用缓存框架的配置

BaseDao.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@CacheConfig(cacheNames = {"mongo"})
public interface BaseDao<T, ID> {
//#root.target.table为SpEL表达式,当前被调用的目标对象实例的table的值
@Cacheable(key = "#root.target.table+#p0",condition ="#root.target.isCache")
T findById(ID id);
@CachePut(key = "#root.target.table+#p0.id",condition ="#root.target.isCache")
<S extends T> S save(S entity);
@CacheEvict(key = "#root.target.table+#p0",condition ="#root.target.isCache")
void deleteById(ID id);
//删除所有是删除mongo所有的表,粒度不能到key
@CacheEvict(key="#root.target.table",allEntries=true,condition="#root.target.isCache")
void deleteAll();
//用来设置是否开启缓存
void enableCache(boolean isCache);
}

BaseDaoImpl.java

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
public class BaseDaoImpl<T, ID> implements BaseDao<T, ID> {
private SimpleMongoRepository<T, ID> mongoRepository;
private Class<T> entityType;
private Class<ID> identifierType;
protected MongoTemplate mongoTemplate;
//这里用来存储表的名字
public String table;
//这里用来判断是否开启redis缓存
public Boolean isCache = false;
//构造方法初始化
public BaseDaoImpl() {
ResolvableType resolvableType = ResolvableType.forClass(getClass());
entityType=(Class<T>)resolvableType.as(BaseDao.class).getGeneric(0).resolve();
identifierType=(Class<ID>)resolvableType.as(BaseDao.class).getGeneric(1).resolve();
//初始化表的名字,用“:”是因为可以在redis里面进行分类
table=entityType.getSimpleName()+":";
}

@Autowired
public void setMongoTemplate(MongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
MappingMongoEntityInformation<T, ID> entityInformation = new MappingMongoEntityInformation<T, ID>(
new BasicMongoPersistentEntity<>(ClassTypeInformation.from(entityType)), identifierType);
mongoRepository = new SimpleMongoRepository<T, ID>(entityInformation, mongoTemplate);
}

@Override
public T findById(ID id) {
return mongoTemplate.findOne(Query.query(Criteria.where("Id").is(id.toString())), entityType);
}

@Override
public void enableCache(boolean isCache) {
this.isCache = isCache;
}
}

下面开始进行使用,新建一个UserDao.jva

1
2
3
public interface UserDao extends BaseDao<User, String> {
void updateAddNumById(String id); //自定义的接口
}

UserDaoImpl.jva

1
2
3
4
5
6
7
8
9
10
11
@Repository
public class UserDaoImpl extends BaseDaoImpl<User, String> implements UserDao {

public UserDaoImpl() {
super.enableCache(true); //这里进开启缓存设置,默认是不开启的
}

@Override
public void updateAddNumById(String id) {
}
}

最好调用findById就会进行缓存了

cYVQv8.png

存在的问题

因为cacheNames也就是表名不支持SpEL,因此获取不到表名,因此设计是,表就用通用mongo字段做完通用表,然后key里面才是表加id的设计,因此也导致了deletAll是删除所有的表,因为deletAll基本不会用到,也还可以接受,就算用到了,只是缓存没了,还是能从数据库重建缓存

参考SpringCache扩展@CacheEvict的key模糊匹配清除

解决方案

新建个该文件CustomizedRedisCacheManager.java

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
public class CustomizedRedisCacheManager extends RedisCacheManager {
private final RedisCacheWriter cacheWriter;
private final RedisCacheConfiguration defaultCacheConfig;


public CustomizedRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
super(cacheWriter, defaultCacheConfiguration);
this.cacheWriter = cacheWriter;
this.defaultCacheConfig = defaultCacheConfiguration;
}

public CustomizedRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, String... initialCacheNames) {
super(cacheWriter, defaultCacheConfiguration, initialCacheNames);
this.cacheWriter = cacheWriter;
this.defaultCacheConfig = defaultCacheConfiguration;
}

public CustomizedRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, boolean allowInFlightCacheCreation, String... initialCacheNames) {
super(cacheWriter, defaultCacheConfiguration, allowInFlightCacheCreation, initialCacheNames);
this.cacheWriter = cacheWriter;
this.defaultCacheConfig = defaultCacheConfiguration;
}

public CustomizedRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, Map<String, RedisCacheConfiguration> initialCacheConfigurations) {
super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations);
this.cacheWriter = cacheWriter;
this.defaultCacheConfig = defaultCacheConfiguration;
}

public CustomizedRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, Map<String, RedisCacheConfiguration> initialCacheConfigurations, boolean allowInFlightCacheCreation) {
super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations, allowInFlightCacheCreation);
this.cacheWriter = cacheWriter;
this.defaultCacheConfig = defaultCacheConfiguration;
}

/**
* 这个构造方法最重要
**/
public CustomizedRedisCacheManager(RedisConnectionFactory redisConnectionFactory, RedisCacheConfiguration cacheConfiguration) {
this(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory), cacheConfiguration);
}

@Override
public Map<String, RedisCacheConfiguration> getCacheConfigurations() {
Map<String, RedisCacheConfiguration> configurationMap = new HashMap<>(getCacheNames().size());
getCacheNames().forEach(it -> {
RedisCache cache = CustomizedRedisCache.class.cast(lookupCache(it));
configurationMap.put(it, cache != null ? cache.getCacheConfiguration() : null);
});
return Collections.unmodifiableMap(configurationMap);
}

@Override
protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
return new CustomizedRedisCache(name, cacheWriter, cacheConfig != null ? cacheConfig : defaultCacheConfig);
}
}

新建CustomizedRedisCache.java

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 class CustomizedRedisCache extends RedisCache {
private final String name;
private final RedisCacheWriter cacheWriter;
private final ConversionService conversionService;

/**
* Create new {@link RedisCache}.
*
* @param name must not be {@literal null}.
* @param cacheWriter must not be {@literal null}.
* @param cacheConfig must not be {@literal null}.
*/
protected CustomizedRedisCache(String name, RedisCacheWriter cacheWriter, RedisCacheConfiguration cacheConfig) {
super(name, cacheWriter, cacheConfig);
this.name = name;
this.cacheWriter = cacheWriter;
this.conversionService = cacheConfig.getConversionService();
}

@Override
public void evict(Object key) {
if (key instanceof String) {
String keyString = key.toString();
// 后缀删除
if (keyString.endsWith("*")) {
byte[] pattern = this.conversionService.convert(this.createCacheKey(key), byte[].class);
this.cacheWriter.clean(this.name, pattern);
return;
}
}
// 删除指定的key
super.evict(key);
}
}

添加配置CachingConfig.java,指定自定义的缓存类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
public class CachingConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
//两个转换器二选一,也可以自定义
//fastJson转换器
FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
//jackson转换器
// Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
// ObjectMapper objectMapper = new ObjectMapper();
// objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
// serializer.setObjectMapper(objectMapper);
RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration
.defaultCacheConfig()
//添加下面这句存到redis里面的数据就是以json格式,不添加就是二进制格式缓存。
//为了解决二进制格式下list数据丢失,改成以json存储
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer));
return new CustomizedRedisCacheManager(redisConnectionFactory, cacheConfiguration);
}
}

最后再修改使用@CacheEvict就支持*号模糊删除了

1
2
//删除table开头的所有key
@CacheEvict(key = "#root.target.table+'*'",condition ="#root.target.isCache")

参考

史上超详细的SpringBoot整合Cache使用教程