springBoot项目中使用缓存Cache
前言
缓存可以通过将经常访问的数据存储在内存中,减少底层数据源如数据库的压力,从而有效提高系统的性能和稳定性。我想大家的项目中或多或少都有使用过,我们项目也不例外,但是最近在review公司的代码的时候写的很蠢且low, 大致写法如下:
public User getById(String id) { User user = cache.getUser(); if(user != null) { return user; } // 从数据库获取 user = loadFromDB(id); cahce.put(id, user); return user; }
其实Spring Boot 提供了强大的缓存抽象,可以轻松地向您的应用程序添加缓存。本文就讲讲如何使用 Spring 提供的不同缓存注解实现缓存的最佳实践。
启用缓存@EnableCaching
现在大部分项目都是是SpringBoot项目,我们可以在启动类添加注解@EnableCaching来开启缓存功能。
@SpringBootApplication @EnableCaching public class SpringCacheApp { public static void main(String[] args) { SpringApplication.run(Cache.class, args); } }
既然要能使用缓存,就需要有一个缓存管理器Bean,默认情况下,@EnableCaching 将注册一个ConcurrentMapCacheManager的Bean,不需要单独的 bean 声明。ConcurrentMapCacheManager将值存储在ConcurrentHashMap的实例中,这是缓存机制的最简单的线程安全实现。
自定义缓存管理器
默认的缓存管理器并不能满足需求,因为她是存储在jvm内存中的,那么如何存储到redis中呢?这时候需要添加自定义的缓存管理器。
添加依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
配置Redis缓存管理器
@Configuration @EnableCaching public class CacheConfig { @Bean public RedisConnectionFactory redisConnectionFactory() { return new LettuceConnectionFactory(); } @Bean public CacheManager cacheManager(RedisConnectionFactory connectionFactory) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofHours(1)); // 设置缓存项的默认过期时间为1小时 return RedisCacheManager.builder(connectionFactory) .cacheDefaults(config) .transactionAware() .build(); } //设置redisTemplate,通过redisUtil调用 @Bean(name = "redisTemplate") public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){ RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); GenericJackson2JsonRedisSerializer JsonRedisSerializer = new GenericJackson2JsonRedisSerializer(); redisTemplate.setConnectionFactory(redisConnectionFactory); redisTemplate.setValueSerializer(JsonRedisSerializer); redisTemplate.setKeySerializer(stringRedisSerializer); redisTemplate.setHashKeySerializer(stringRedisSerializer); redisTemplate.setHashValueSerializer(JsonRedisSerializer); return redisTemplate; } }
现在有了缓存管理器以后,我们如何在业务层面操作缓存呢?
我们可以使用@Cacheable、@CachePut 或@CacheEvict 注解来操作缓存了。
@Cacheable
该注解可以将方法运行的结果进行缓存,在缓存时效内再次调用该方法时不会调用方法本身,而是直接从缓存获取结果并返回给调用方。
例子1:缓存数据库查询的结果。
@Service public class MyService { @Autowired private MyRepository repository; @Cacheable(value = "myCache", key = "#id") public MyEntity getEntityById(Long id) { return repository.findById(id).orElse(null); } }
在此示例中,@Cacheable 注解用于缓存 getEntityById()方法的结果,该方法根据其 ID 从数据库中检索 MyEntity 对象。
但是如果我们更新数据呢?旧数据仍然在缓存中?
@CachePut
然后@CachePut 出来了, 与 @Cacheable 注解不同的是使用 @CachePut 注解标注的方法,在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式写入指定的缓存中。@CachePut 注解一般用于更新缓存数据,相当于缓存使用的是写模式中的双写模式。
@Service public class MyService { @Autowired private MyRepository repository; @CachePut(value = "myCache", key = "#entity.id") public void saveEntity(MyEntity entity) { repository.save(entity); } }
@CacheEvict
标注了 @CacheEvict 注解的方法在被调用时,会从缓存中移除已存储的数据。@CacheEvict 注解一般用于删除缓存数据,相当于缓存使用的是写模式中的失效模式。
@Service public class MyService { @Autowired private MyRepository repository; @CacheEvict(value = "myCache", key = "#id") public void deleteEntityById(Long id) { repository.deleteById(id); } }
@Caching
@Caching 注解用于在一个方法或者类上,同时指定多个 Spring Cache 相关的注解。
例子1:@Caching注解中的evict属性指定在调用方法 saveEntity 时失效两个缓存。
@Service public class MyService { @Autowired private MyRepository repository; @Cacheable(value = "myCache", key = "#id") public MyEntity getEntityById(Long id) { return repository.findById(id).orElse(null); } @Caching(evict = { @CacheEvict(value = "myCache", key = "#entity.id"), @CacheEvict(value = "otherCache", key = "#entity.id") }) public void saveEntity(MyEntity entity) { repository.save(entity); } }
例子2:调用getEntityById方法时,Spring会先检查结果是否已经缓存在myCache缓存中。如果是,Spring 将返回缓存的结果而不是执行该方法。如果结果尚未缓存,Spring 将执行该方法并将结果缓存在 myCache 缓存中。方法执行后,Spring会根据@CacheEvict注解从otherCache缓存中移除缓存结果。
@Service public class MyService { @Caching( cacheable = { @Cacheable(value = "myCache", key = "#id") }, evict = { @CacheEvict(value = "otherCache", key = "#id") } ) public MyEntity getEntityById(Long id) { return repository.findById(id).orElse(null); } }
例子3:当调用saveData方法时,Spring会根据@CacheEvict注解先从otherCache缓存中移除数据。然后,Spring 将执行该方法并将结果保存到数据库或外部 API。
方法执行后,Spring 会根据@CachePut注解将结果添加到 myCache、myOtherCache 和 myThirdCache 缓存中。Spring 还将根据@Cacheable注解检查结果是否已缓存在 myFourthCache 和 myFifthCache 缓存中。如果结果尚未缓存,Spring 会将结果缓存在适当的缓存中。如果结果已经被缓存,Spring 将返回缓存的结果,而不是再次执行该方法。
@Service public class MyService { @Caching( put = { @CachePut(value = "myCache", key = "#result.id"), @CachePut(value = "myOtherCache", key = "#result.id"), @CachePut(value = "myThirdCache", key = "#result.name") }, evict = { @CacheEvict(value = "otherCache", key = "#id") }, cacheable = { @Cacheable(value = "myFourthCache", key = "#id"), @Cacheable(value = "myFifthCache", key = "#result.id") } ) public MyEntity saveData(Long id, String name) { // Code to save data to a database or external API MyEntity entity = new MyEntity(id, name); return entity; } }
@CacheConfig
通过@CacheConfig 注解,我们可以将一些缓存配置简化到类级别的一个地方,这样我们就不必多次声明相关值:
@CacheConfig(cacheNames={"myCache"}) @Service public class MyService { @Autowired private MyRepository repository; @Cacheable(key = "#id") public MyEntity getEntityById(Long id) { return repository.findById(id).orElse(null); } @CachePut(key = "#entity.id") public void saveEntity(MyEntity entity) { repository.save(entity); } @CacheEvict(key = "#id") public void deleteEntityById(Long id) { repository.deleteById(id); } }
Condition & Unless
condition作用:指定缓存的条件(满足什么条件才缓存),可用 SpEL 表达式(如 #id>0,表示当入参 id 大于 0 时才缓存)
unless作用 : 否定缓存,即满足 unless 指定的条件时,方法的结果不进行缓存,使用 unless 时可以在调用的方法获取到结果之后再进行判断(如 #result == null,表示如果结果为 null 时不缓存)
//when id >10, the @CachePut works. @CachePut(key = "#entity.id", condition="#entity.id > 10") public void saveEntity(MyEntity entity) { repository.save(entity); } //when result != null, the @CachePut works. @CachePut(key = "#id", condition="#result == null") public void saveEntity1(MyEntity entity) { repository.save(entity); }
清理全部缓存
通过allEntries、beforeInvocation属性可以来清除全部缓存数据,不过allEntries是方法调用后清理,beforeInvocation是方法调用前清理。
//方法调用完成之后,清理所有缓存 @CacheEvict(value="myCache",allEntries=true) public void delectAll() { repository.deleteAll(); } //方法调用之前,清除所有缓存 @CacheEvict(value="myCache",beforeInvocation=true) public void delectAll() { repository.deleteAll(); }
SpEL表达式
Spring Cache注解中频繁用到SpEL表达式,那么具体如何使用呢?
SpEL 表达式的语法
Spring Cache可用的变量
最佳实践
通过Spring缓存注解可以快速优雅地在我们项目中实现缓存的操作,但是在双写模式或者失效模式下,可能会出现缓存数据一致性问题(读取到脏数据),Spring Cache 暂时没办法解决。最后我们再总结下Spring Cache使用的一些最佳实践。
只缓存经常读取的数据:缓存可以显着提高性能,但只缓存经常访问的数据很重要。很少或从不访问的缓存数据会占用宝贵的内存资源,从而导致性能问题。
根据应用程序的特定需求选择合适的缓存提供程序和策略。SpringBoot 支持多种缓存提供程序,包括 Ehcache、Hazelcast 和 Redis。
使用缓存时请注意潜在的线程安全问题。对缓存的并发访问可能会导致数据不一致或不正确,因此选择线程安全的缓存提供程序并在必要时使用适当的同步机制非常重要。
避免过度缓存。缓存对于提高性能很有用,但过多的缓存实际上会消耗宝贵的内存资源,从而损害性能。在缓存频繁使用的数据和允许垃圾收集不常用的数据之间取得平衡很重要。
使用适当的缓存逐出策略。使用缓存时,重要的是定义适当的缓存逐出策略以确保在必要时从缓存中删除旧的或陈旧的数据。
使用适当的缓存键设计。缓存键对于每个数据项都应该是唯一的,并且应该考虑可能影响缓存数据的任何相关参数,例如用户 ID、时间或位置。
常规数据(读多写少、即时性与一致性要求不高的数据)完全可以使用 Spring Cache,至于写模式下缓存数据一致性问题的解决,只要缓存数据有设置过期时间就足够了。
特殊数据(读多写多、即时性与一致性要求非常高的数据),不能使用 Spring Cache,建议考虑特殊的设计(例如使用 Cancal 中间件等)。
附redisUtil封装工具,
package cn.wmadmin.cbam.common.util; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import javax.annotation.Resource; import java.util.*; import java.util.concurrent.TimeUnit; /** * Redis工具 */ @Component public class RedisUtil { private static RedisTemplate<String, Object> redisTemplate; private static final String redisPrefix ="wm:"; /** * 注入Redis * * @author wm * @param redisTemplate Redis对象 */ @Resource public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) { RedisUtil.redisTemplate = redisTemplate; } /** * 对象句柄 * * @author wm * @return RedisTemplate */ public static RedisTemplate<String, Object> handler() { return redisTemplate; } /** * 指定缓存失效时间 * * @author wm * @param key 键 * @param second 时间(秒) */ public static void expire(String key, Long second) { key = redisPrefix + key; redisTemplate.expire(key, second, TimeUnit.SECONDS); } /** * 指定缓存失效时间 * * @author wm * @param key 键 * @param millisecond 时间(毫秒) */ public static void pExpire(String key, Long millisecond) { key = redisPrefix + key; redisTemplate.expire(key, millisecond, TimeUnit.MILLISECONDS); } /** * 指定缓存永久有效 * * @author wm * @param key 键 */ public static void persist(String key) { key = redisPrefix + key; redisTemplate.persist(key); } /** * 根据key获取过期时间 * * @author wm * @param key 键不能为null * @return 返回0代表为永久有效(秒) */ public static Long ttl(String key) { key = redisPrefix + key; return redisTemplate.getExpire(key, TimeUnit.SECONDS); } /** * 根据key获取过期时间 * * @author wm * @param key 键不能为null * @return 返回0代表为永久有效(毫秒) */ public static Long pTtl(String key) { key = redisPrefix + key; return redisTemplate.getExpire(key, TimeUnit.MILLISECONDS); } /** * 判断key是否存在 * * @author wm * @param key 键 * @return true=存在,false=不存在 */ public static Boolean exists(String key) { key = redisPrefix + key; return redisTemplate.hasKey(key); } /** * 删除1个或多个键 * * @author wm * @param key 键(一个或多个) */ @SuppressWarnings("unchecked") public static void del(String... key) { if (key.length == 1) { key[0] = redisPrefix + key[0]; redisTemplate.delete(key[0]); } else { for (int i=0; key.length > i; i++) { key[i] = redisPrefix + key[i]; } redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key)); } } /** * 给key赋值一个新的key名 * * @author wm * @param oldKey 旧的key * @param newKey 新的key */ public static void rename(String oldKey, String newKey) { oldKey = redisPrefix + oldKey; newKey = redisPrefix + newKey; redisTemplate.rename(oldKey, newKey); } /** * 将当前数据库的key移动到给定的数据库db当中 * * @author wm * @param key 键 * @param db 库 * @return Boolean */ public static Boolean move(String key, int db) { key = redisPrefix + key; return redisTemplate.move(key, db); } /** * 获取匹配的key值 * * @author wm * @author wm * @param pattern 通配符(*, ?, []) * @return Set */ public static Set<String> keys(String pattern) { return redisTemplate.keys(pattern); } /** * 随机返回一个key * * @author wm * @author wm * @return String */ public static String randomKey() { return redisTemplate.randomKey(); } /* ***************** common end *************** */ /** * 按匹配获取或有KEY * * @author wm * @param pattern 规则 * @return Set<String> */ public static Set<String> matchSet(String pattern) { Set<String> keys = new LinkedHashSet<>(); RedisUtil.handler().execute((RedisConnection connection) -> { try (Cursor<byte[]> cursor = connection.scan( ScanOptions.scanOptions() .count(Long.MAX_VALUE) .match(pattern) .build() )) { cursor.forEachRemaining(item -> { keys.add(RedisSerializer.string().deserialize(item)); }); return null; } catch (Exception e) { throw new RuntimeException(e); } }); return keys; } /** * 获取key的值 * * @author wm * @param key 键 * @return Object */ public static Object get(String key) { key = redisPrefix + key; return redisTemplate.opsForValue().get(key); } /** * 获取旧值并设置新值 * * @author wm * @param key 键 * @param newVal 新值 * @return Object */ public static Object getSet(String key, Object newVal) { key = redisPrefix + key; return redisTemplate.opsForValue().getAndSet(key, newVal); } /** * 设置键值对 * * @author wm * @param key 键 * @param value 值 */ public static void set(String key, Object value) { key = redisPrefix + key; redisTemplate.opsForValue().set(key, value); } /** * 设置键值对并设置时间 * * @author wm * @param key 键 * @param value 值 * @param time time要大于0 如果time小于等于0 将设置无限期 */ public static void set(String key, Object value, long time) { key = redisPrefix + key; if (time > 0) { redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); } else { set(key, value); } } /** * 递增 * * @author wm * @param key 键 * @param delta 要增加几(大于0) * @return Long */ public static Long incr(String key, long delta) { if (delta < 0) { throw new RuntimeException("递增因子必须大于0"); } key = redisPrefix + key; return redisTemplate.opsForValue().increment(key, delta); } /** * 递减 * * @author wm * @param key 键 * @param delta 要减少几(小于0) * @return Long */ public static Long decr(String key, long delta) { if (delta < 0) { throw new RuntimeException("递减因子必须大于0"); } key = redisPrefix + key; return redisTemplate.opsForValue().increment(key, -delta); } /* ***************** String end *************** */ /** * 获取key中field域的值 * * @author wm * @param key 键 不能为null * @param field 项 不能为null * @return 值 */ public static Object hGet(String key, String field) { key = redisPrefix + key; return redisTemplate.opsForHash().get(key, field); } /** * 判断key中有没有field域名 * * @author wm * @param key 键 * @param field 字段 * @return Boolean */ public static Boolean hExists(String key, Object field) { key = redisPrefix + key; return redisTemplate.opsForHash().hasKey(key, field); } /** * 获取hashKey对应的所有键值 * * @author wm * @param key 键 * @return 对应的多个键值 */ public Map<Object, Object> hmGet(String key) { key = redisPrefix + key; return redisTemplate.opsForHash().entries(key); } /** * 设置field1->N个域,对应的值是value1->N * * @author wm * @param key 键 * @param map 对应多个键值 */ public static void hmSet(String key, Map<String, Object> map) { key = redisPrefix + key; redisTemplate.opsForHash().putAll(key, map); } /** * HashSet 并设置时间 * * @author wm * @param key 键 * @param map 对应多个键值 * @param time 时间(秒) */ public static void hmSet(String key, Map<String, Object> map, long time) { key = redisPrefix + key; redisTemplate.opsForHash().putAll(key, map); if (time > 0) { expire(key, time); } } /** * 向一张hash表中放入数据,如果不存在将创建 * * @author wm * @param key 键 * @param item 项 * @param value 值 */ public static void hSet(String key, String item, Object value) { key = redisPrefix + key; redisTemplate.opsForHash().put(key, item, value); } /** * 向一张hash表中放入数据,如果不存在将创建 * * @author wm * @param key 键 * @param item 项 * @param value 值 * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间 * @return true 成功 false失败 */ public static boolean hSet(String key, String item, Object value, long time) { key = redisPrefix + key; redisTemplate.opsForHash().put(key, item, value); if (time > 0) { expire(key, time); } return true; } /** * 删除hash表中的值 * * @author wm * @param key 键 不能为null * @param item 项 可以使多个 不能为null */ public static void hDel(String key, Object... item) { key = redisPrefix + key; redisTemplate.opsForHash().delete(key, item); } /** * 判断hash表中是否有该项的值 * * @author wm * @param key 键 不能为null * @param item 项 不能为null * @return true 存在 false不存在 */ public static boolean hHasKey(String key, String item) { key = redisPrefix + key; return redisTemplate.opsForHash().hasKey(key, item); } /** * hash递增 如果不存在,就会创建一个并把新增后的值返回 * * @author wm * @param key 键 * @param item 项 * @param by 要增加几(大于0) * @return double */ public static double hIncr(String key, String item, long by) { key = redisPrefix + key; return redisTemplate.opsForHash().increment(key, item, by); } /** * hash递减 * * @author wm * @param key 键 * @param item 项 * @param by 要减少记(小于0) * @return double */ public static double hDecr(String key, String item, long by) { key = redisPrefix + key; return redisTemplate.opsForHash().increment(key, item, -by); } /* ***************** Map end *************** */ /** * 根据key获取Set中的所有值 * * @author wm * @param key 键 * @return Set */ public static Set<Object> sGet(String key) { key = redisPrefix + key; return redisTemplate.opsForSet().members(key); } /** * 根据value从一个set中查询,是否存在 * * @author wm * @param key 键 * @param value 值 * @return true 存在 false不存在 */ public Boolean sHasKey(String key, Object value) { key = redisPrefix + key; return redisTemplate.opsForSet().isMember(key, value); } /** * 将数据放入set缓存 * * @author wm * @param key 键 * @param values 值 可以是多个 * @return 成功个数 */ public static Long sSet(String key, Object... values) { key = redisPrefix + key; return redisTemplate.opsForSet().add(key, values); } /** * 将set数据放入缓存 * * @author wm * @param key 键 * @param time 时间(秒) * @param values 值 可以是多个 * @return 成功个数 */ public Long sSetAndTime(String key, long time, Object... values) { key = redisPrefix + key; return redisTemplate.opsForSet().add(key, values); } /** * 获取set缓存的长度 * * @author wm * @param key 键 * @return Long */ public Long sGetSetSize(String key) { key = redisPrefix + key; return redisTemplate.opsForSet().size(key); } /** * 移除值为value的 * * @author wm * @param key 键 * @param values 值 可以是多个 * @return 移除的个数 */ public Long setRemove(String key, Object... values) { key = redisPrefix + key; return redisTemplate.opsForSet().remove(key, values); } /* ***************** Set end *************** */ /** * 获取list缓存的内容 * * @author wm * @param key 键 * @param start 开始 * @param end 结束 0 到 -1代表所有值 * @return List */ public List<Object> lGet(String key, long start, long end) { key = redisPrefix + key; return redisTemplate.opsForList().range(key, start, end); } /** * 获取list缓存的长度 * * @author wm * @param key 键 * @return Long */ public Long lGetListSize(String key) { key = redisPrefix + key; return redisTemplate.opsForList().size(key); } /** * 通过索引获取list中的值 * * @author wm * @param key 键 * @param index 索引 index>=0时,0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推 * @return Object */ public Object lGetIndex(String key, long index) { key = redisPrefix + key; return redisTemplate.opsForList().index(key, index); } /** * 将list放入缓存 * * @author wm * @param key 键 * @param value 值 * @return boolean */ public boolean lSet(String key, Object value) { key = redisPrefix + key; redisTemplate.opsForList().rightPush(key, value); return true; } /** * 将list放入缓存 * * @author wm * @param key 键 * @param value 值 * @param second 时间(秒) * @return boolean */ public boolean lSet(String key, Object value, long second) { key = redisPrefix + key; redisTemplate.opsForList().rightPush(key, value); if (second > 0) expire(key, second); return true; } /** * 将list放入缓存 * * @author wm * @param key 键 * @param value 值 * @return boolean */ public boolean lSet(String key, List<Object> value) { key = redisPrefix + key; redisTemplate.opsForList().rightPushAll(key, value); return true; } /** * 将list放入缓存 * * @author wm * @param key 键 * @param value 值 * @param time 时间(秒) * @return boolean */ public boolean lSet(String key, List<Object> value, Long time) { key = redisPrefix + key; redisTemplate.opsForList().rightPushAll(key, value); if (time > 0) expire(key, time); return true; } /** * 根据索引修改list中的某条数据 * * @author wm * @param key 键 * @param index 索引 * @param value 值 * @return boolean */ public boolean lUpdateIndex(String key, Long index, Object value) { key = redisPrefix + key; redisTemplate.opsForList().set(key, index, value); return true; } /** * 移除N个值为value * * @author wm * @param key 键 * @param count 移除多少个 * @param value 值 * @return 移除的个数 */ public Long lRemove(String key, Long count, Object value) { key = redisPrefix + key; return redisTemplate.opsForList().remove(key, count, value); } }