之前怎么加缓存
为了加快数据获取速度,减少数据库的I/O压力,往往在业务接口中加入缓存。相信大多数人都是按照下面的方法中操作缓存:
public User getUserById(long id) {
//缓存中获取数据
if (Cache.contain(CacheKey.get(id))) {
return Cache.get(id);
}
//数据库获取数据
User user = userDao.getById();
Cache.put(CacheKey.get(id),user);
return user;
}
缓存的读取和保存已经侵入到业务代码中,后期很难维护。那么,是否有一种优雅的方法,无侵入地解决缓存问题呢?
spring提供了spring cache模块,利用注解式AOP的方式提供缓存支持。
实现
假设需要对用户业务接口的查询功能加入缓存,在更新用户信息和删除用户的时候清除缓存,保证数据一致性。为了演示,定义了用户业务类UserService。
public interface UserService {
User getById(long id);
void deleteById(long id);
void update(User user);
}
###(1)业务添加缓存
配置cache的规则,例如key,cache的名字,什么情况下保存或者删除缓存。这些规则都支持SpEL表达式,关于SpEL表达式请自行了解。先看一下为了满足需求,对业务的缓存配置如下:
@Service
@CacheConfig(cacheNames = {"userCache"})
public class UserServiceImpl implements UserService {
@Cacheable(key = "'user:'+#id", unless = "#result == null")
@Override
public User getById(long id) {
System.out.println("getById():" + id);
return new User(id, "hugo");
}
@CacheEvict(key = "'user:'+#id")
@Override
public void deleteById(long id) {
System.out.println("deleteById():" + id);
throw new RuntimeException("deleteById()方法发生了异常");
}
@CacheEvict(key = "'user:'+#user.id")
@Override
public void update(User user) {
System.out.println("update():" + user.toString());
}
}
上面使用名为userCache的缓存实例,为了演示,三个业务方法的缓存key都是以"user:id"的方式定义,这里使用SpEL表达式,spring会在运行时获取id值填入到表达式中。SpEL支持如下上下文数据:
** cache注解详细解释 **
-
@Cacheable 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存
-
@CachePut 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用
-
@CacheEvict 主要针对方法配置,能够根据一定的条件对缓存进行清空
(2)spring配置缓存
前面已经在业务方法中应用了缓存,那么接下来将在spring中定义该缓存实例。由于现在在项目中主要应用ehcache和redis作为缓存实现方案,所以下面分别对ehcache和redis两种方案的缓存进行配置。
** ehcache **
清单:applicationContext-cache-ehcache.xml
<!-- 启用缓存注解功能,这个是必须的,否则注解不会生效,另外,该注解一定要声明在spring主配置文件中才会生效 -->
<!--使用proxy方式,内部调用时,被内部调用的方法的缓存失效-->
<!--使用aspect方式,需要按照https://my.oschina.net/thinwonton/blog/918006的方法配置-->
<cache:annotation-driven cache-manager="cacheManager" mode="proxy" proxy-target-class="true"/>
<!-- cacheManager工厂类,指定ehcache.xml的位置 -->
<bean id="cacheManagerFactory" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
<property name="configLocation" value="classpath:cache/ehcache.xml"/>
</bean>
<!-- 声明cacheManager -->
<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
<property name="cacheManager" ref="cacheManagerFactory"/>
</bean>
清单:ehcache.xml
<?xml version="1.0" encoding="UTF-8"?>
<ehcache updateCheck="false">
<!--
<diskStore path="java.io.tmpdir" />
-->
<diskStore path="E:/cachetmpdir"/>
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
maxElementsOnDisk="10000000"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"/>
<cache name="userCache"
maxElementsInMemory="10000"
maxElementsOnDisk="1000"
eternal="false"
diskPersistent="true"
overflowToDisk="true"
diskSpoolBufferSizeMB="20"
timeToIdleSeconds="300"
timeToLiveSeconds="600"
memoryStoreEvictionPolicy="LRU"/>
</ehcache>
** redis **
这里需要注意的是,key和value的生成方式。key 的生成器使用org.springframework.data.redis.serializer.StringRedisSerializer,该生成器获取到的key可以在任意redis客户端中通用;value使用GenericJackson2JsonRedisSerializer生成,因为jackson反序列化时需要指明类的类型,所以GenericJackson2JsonRedisSerializer将会在序列化的时候保存class类型到redis中。
另外,RedisCacheManager默认情况下是不支持单独对某个key(方法级别)设置过期时间的,这个需要进行扩展。配置文件中的expires属性的意思是对某个缓存实例设置过期时间。
清单:applicationContext-cache-redis.xml
<!-- 启用缓存注解功能,这个是必须的,否则注解不会生效,另外,该注解一定要声明在spring主配置文件中才会生效 -->
<!--使用proxy方式,内部调用时,被内部调用的方法的缓存失效-->
<!--使用aspect方式,需要按照https://my.oschina.net/thinwonton/blog/918006的方法配置-->
<cache:annotation-driven cache-manager="cacheManager" mode="proxy"/>
<!-- redis连接池的配置 -->
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxIdle" value="1"/>
<property name="maxTotal" value="5"/>
<property name="blockWhenExhausted" value="true"/>
<property name="maxWaitMillis" value="30000"/>
<property name="testOnBorrow" value="true"/>
</bean>
<!-- 工厂类配置 -->
<bean id="jedisConnectionFactory"
class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property name="hostName" value="192.168.88.18"/>
<property name="port" value="6379"/>
<property name="poolConfig" ref="jedisPoolConfig"/>
<property name="timeout" value="15000"/>
<property name="usePool" value="true"/>
</bean>
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<property name="connectionFactory" ref="jedisConnectionFactory"/>
<property name="keySerializer">
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
</property>
<property name="valueSerializer">
<bean class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer"/>
</property>
<property name="hashKeySerializer">
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
</property>
<property name="hashValueSerializer">
<bean class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer"/>
</property>
</bean>
<bean id="cacheManager" class="org.springframework.data.redis.cache.RedisCacheManager">
<constructor-arg ref="redisTemplate"/>
<constructor-arg name="cacheNames">
<set>
<!--声明userCache-->
<value>userCache</value>
</set>
</constructor-arg>
<!-- 是否在容器启动时初始化 -->
<property name="loadRemoteCachesOnStartup" value="true"/>
<!-- 是否使用前缀,规则是 cacheName:key-->
<property name="usePrefix" value="true"/>
<!-- 前缀命名,仅当usePrefix为true时才生效 -->
<property name="cachePrefix">
<bean class="org.springframework.data.redis.cache.DefaultRedisCachePrefix">
<constructor-arg name="delimiter" value=":"/>
</bean>
</property>
<!-- 默认有效期1h -->
<property name="defaultExpiration" value="3600"/>
<property name="expires">
<map>
<!--对cahce实例设置过期时间-->
<entry key="userCache" value="10"/>
</map>
</property>
</bean>
运行测试结果如下,在redis中存储了key为userCache:user:100的缓存数据,缓存的有效期是3600秒。