spring注解式缓存+Redis的前提条件:spring+redis集成
<!-- ********************** redis相关依赖 ********************** -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>${jedis.version}</version>
<exclusions>
<exclusion>
<artifactId>commons-pool2</artifactId>
<groupId>org.apache.commons</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- spring-data-redis 依赖-->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>${spring-data-redis.version}</version>
</dependency>
#ip\u5730\u5740
redis.hostName=192.168.199.128
#\u7AEF\u53E3\u53F7
redis.port=6379
#\u5982\u679C\u6709\u5BC6\u7801
redis.password=123456
#\u5BA2\u6237\u7AEF\u8D85\u65F6\u65F6\u95F4\u5355\u4F4D\u662F\u6BEB\u79D2 \u9ED8\u8BA4\u662F2000
redis.timeout=10000
#redis\u7F13\u5B58\u6570\u636E\u8FC7\u671F\u65F6\u95F4\u5355\u4F4D\u79D2
redis.expiration=3600
##################### redis\u8FDE\u63A5\u6C60\u914D\u7F6E ###########################################
#\u6700\u5927\u7A7A\u95F2\u6570
redis.maxIdle=300
#\u8FDE\u63A5\u6C60\u7684\u6700\u5927\u6570\u636E\u5E93\u8FDE\u63A5\u6570\u3002\u8BBE\u4E3A0\u8868\u793A\u65E0\u9650\u5236,\u5982\u679C\u662Fjedis 2.4\u4EE5\u540E\u7528redis.maxTotal
#redis.maxActive=600
#\u63A7\u5236\u4E00\u4E2Apool\u53EF\u5206\u914D\u591A\u5C11\u4E2Ajedis\u5B9E\u4F8B,\u7528\u6765\u66FF\u6362\u4E0A\u9762\u7684redis.maxActive,\u5982\u679C\u662Fjedis 2.4\u4EE5\u540E\u7528\u8BE5\u5C5E\u6027
redis.maxTotal=1000
#\u6700\u5927\u5EFA\u7ACB\u8FDE\u63A5\u7B49\u5F85\u65F6\u95F4\u3002\u5982\u679C\u8D85\u8FC7\u6B64\u65F6\u95F4\u5C06\u63A5\u5230\u5F02\u5E38\u3002\u8BBE\u4E3A-1\u8868\u793A\u65E0\u9650\u5236\u3002
redis.maxWaitMillis=1000
#\u8FDE\u63A5\u7684\u6700\u5C0F\u7A7A\u95F2\u65F6\u95F4 \u9ED8\u8BA41800000\u6BEB\u79D2(30\u5206\u949F)
redis.minEvictableIdleTimeMillis=300000
#\u6BCF\u6B21\u91CA\u653E\u8FDE\u63A5\u7684\u6700\u5927\u6570\u76EE,\u9ED8\u8BA43
redis.numTestsPerEvictionRun=1024
#\u9010\u51FA\u626B\u63CF\u7684\u65F6\u95F4\u95F4\u9694(\u6BEB\u79D2) \u5982\u679C\u4E3A\u8D1F\u6570,\u5219\u4E0D\u8FD0\u884C\u9010\u51FA\u7EBF\u7A0B, \u9ED8\u8BA4-1
redis.timeBetweenEvictionRunsMillis=30000
#\u662F\u5426\u5728\u4ECE\u6C60\u4E2D\u53D6\u51FA\u8FDE\u63A5\u524D\u8FDB\u884C\u68C0\u9A8C,\u5982\u679C\u68C0\u9A8C\u5931\u8D25,\u5219\u4ECE\u6C60\u4E2D\u53BB\u9664\u8FDE\u63A5\u5E76\u5C1D\u8BD5\u53D6\u51FA\u53E6\u4E00\u4E2A
redis.testOnBorrow=true
#\u5728\u7A7A\u95F2\u65F6\u68C0\u67E5\u6709\u6548\u6027, \u9ED8\u8BA4false
redis.testWhileIdle=true
#redis\u96C6\u7FA4\u914D\u7F6E
#redis.sentinel.host1=172.20.1.230
#redis.sentinel.port1=26379
#redis.sentinel.host2=172.20.1.231
#redis.sentinel.port2=26379
#redis.sentinel.host3=172.20.1.232
#redis.sentinel.port3=26379
spring-redis.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:cache="http://www.springframework.org/schema/cache"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd">
<!-- redis的相关配置已经在applicationContext.xml导入了,因为spring只允许有一个context:property-placeholder -->
<!-- 所以下面的配置会注释掉了 -->
<!-- 1. 引入properties配置文件 -->
<!--<context:property-placeholder ignore-unresolvable="true" location="classpath:redis.properties" />-->
<!-- 2. redis连接池配置-->
<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
<!--最大空闲数-->
<property name="maxIdle" value="${redis.maxIdle}"/>
<!--连接池的最大数据库连接数 -->
<property name="maxTotal" value="${redis.maxTotal}"/>
<!--最大建立连接等待时间-->
<property name="maxWaitMillis" value="${redis.maxWaitMillis}"/>
<!--逐出连接的最小空闲时间 默认1800000毫秒(30分钟)-->
<property name="minEvictableIdleTimeMillis" value="${redis.minEvictableIdleTimeMillis}"/>
<!--每次逐出检查时 逐出的最大数目 如果为负数就是 : 1/abs(n), 默认3-->
<property name="numTestsPerEvictionRun" value="${redis.numTestsPerEvictionRun}"/>
<!--逐出扫描的时间间隔(毫秒) 如果为负数,则不运行逐出线程, 默认-1-->
<property name="timeBetweenEvictionRunsMillis" value="${redis.timeBetweenEvictionRunsMillis}"/>
<!--是否在从池中取出连接前进行检验,如果检验失败,则从池中去除连接并尝试取出另一个-->
<property name="testOnBorrow" value="${redis.testOnBorrow}"/>
<!--在空闲时检查有效性, 默认false -->
<property name="testWhileIdle" value="${redis.testWhileIdle}"/>
</bean>
<!-- 3. redis连接工厂 -->
<bean id="connectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
destroy-method="destroy">
<property name="poolConfig" ref="poolConfig"/>
<!--IP地址 -->
<property name="hostName" value="${redis.hostName}"/>
<!--端口号 -->
<property name="port" value="${redis.port}"/>
<!--如果Redis设置有密码 -->
<property name="password" value="${redis.password}"/>
<!--客户端超时时间单位是毫秒 -->
<property name="timeout" value="${redis.timeout}"/>
</bean>
<!-- 4. redis操作模板,使用该对象可以操作redis -->
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<property name="connectionFactory" ref="connectionFactory"/>
<!--如果不配置Serializer,那么存储的时候缺省使用String,如果用User类型存储,那么会提示错误User can't cast to String!! -->
<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>
<!--开启事务 -->
<property name="enableTransactionSupport" value="false"/>
</bean>
</beans>
<bean id="redisCacheManager" class="org.springframework.data.redis.cache.RedisCacheManager">
<constructor-arg name="redisOperations" ref="redisTemplate" />
<!--redis缓存数据过期时间单位秒-->
<property name="defaultExpiration" value="${redis.expiration}" />
<property name="usePrefix" value="true"/>
<property name="cachePrefix">
<bean class="org.springframework.data.redis.cache.DefaultRedisCachePrefix">
<constructor-arg index="0" value="-cache-"/>
</bean>
</property>
</bean>
1.2 配置自定义Key生成器CacheKeyGenerator spring-redis.xml文件
缓存的Java对象一定要重写hashCode和eqauls
<bean id="cacheKeyGenerator" class="com.zking.ssm.redis.CacheKeyGenerator"></bean>
注:CacheKeyGenerator 源码在下面
1.3 启用缓存注解功能 spring-redis.xml文件
<cache:annotation-driven cache-manager="redisCacheManager" key-generator="cacheKeyGenerator"/>
1.4 在需要的地方进行注解缓存
缓存注解
2.1 @CacheConfig
它是一个类级别的注解,允许共享缓存的名称、KeyGenerator、CacheManager和CacheResolver
value:缓存位置的一段名称,不能为空
key:缓存的key,默认为空,表示使用方法的参数类型及参数值作为key,支持SpEL
2.2 @Cacheable
配置在方法或类上,作用:本方法执行后,先去缓存看有没有数据,如果没有,从数据库中查找出来,给缓存中存一份,返回结果,
下次本方法执行,在缓存未过期情况下,先在缓存中查找,有的话直接返回,没有的话从数据库查找
value:缓存位置的一段名称,不能为空
key:缓存的key,默认为空,表示使用方法的参数类型及参数值作为key,支持SpEL
keyGenerator:指定key的生成策略
condition:触发条件,满足条件就加入缓存,默认为空,表示全部都加入缓存,支持SpEL
注1:condition是在方法执行前评估, unless是在方法执行后评估.
2.3 @CachePut
类似于更新操作,即每次不管缓存中有没有结果,都从数据库查找结果,并将结果更新到缓存,并返回结果
value 缓存的名称,在 spring 配置文件中定义,必须指定至少一个
key 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合
condition 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存
2.4 @CacheEvict
用来清除用在本方法或者类上的缓存数据(用在哪里清除哪里)
value:缓存位置的一段名称,不能为空
key:缓存的key,默认为空,表示使用方法的参数类型及参数值作为key,支持SpEL
condition:触发条件,满足条件就加入缓存,默认为空,表示全部都加入缓存,支持SpEL
allEntries:true表示清除value中的全部缓存,默认为false
2.5使用实例:
@CacheEvict(value = "selectByPrimaryKey",key = "'selectOne-'+#id",allEntries = true)
@Override
public int deleteByPrimaryKey(Integer id) {
return bookMapper.deleteByPrimaryKey(id);
}
@Cacheable(value = "selectByPrimaryKey",key = "'selectOne-'+#id",condition = "#id<10")
@Override
public Book selectByPrimaryKey(Integer id) {
return bookMapper.selectByPrimaryKey(id);
}
Spring-Cache key设置
3.1 基本形式
@Cacheable(value=“cacheName”, key=“#id”)
public ResultDTO method(int id);
注1:Spring Cacheable注解不缓存null值
用Cacheable注解时,发现空值,也会被缓存下来。下次另一个系统如果更新了值,这边从缓存取,还是空值,会有问题。
解决方案:
@Cacheable(value = “service”, key = “#service.serviceId.toString()”, unless = “#result == null”)
@Cacheable(value = “service”, keyGenerator = RedisKeys.KEY_GENERATOR, unless = “#result.size() == 0”)
3.2 组合形式
@Cacheable(value=“cacheName”, key="T(String).valueOf(#name).concat(‘-’).concat(#password))
public ResultDTO method(int name, String password);
3.3 对象形式
@Cacheable(value=“cacheName”, key="#user.id)
public ResultDTO method(User user);
3.4 自定义Key生成器
@Cacheable(value=“gomeo2oCache”, keyGenerator = “keyGenerator”)
public ResultDTO method(User user);
spring注解式缓存中的巨坑~~~~~~~
没有指定key,默认情况下spirng会使用SimpleKeyGenerator生成key,
而Spring默认的SimpleKeyGenerator是不会将函数名组合进key中的,举个例子:
@Component
public class CacheTestImpl implements CacheTest {
@Cacheable(“databaseCache”)
public Long test1()
{ return 1L; }
@Cacheable(“databaseCache”)
public Long test2()
{ return 2L; }
@Cacheable(“databaseCache”)
public Long test3()
{ return 3L; }
@Cacheable(“databaseCache”)
public String test4()
{ return “4”; }//注意返回的是字符串“4”
}
我们期望的输出是:
1
2
3
4
而实际上的输出是:
1
1
1
ClassCastException: java.lang.Long cannot be cast to java.lang.String
此外,原子类型的数组,直接作为key使用也是不会生效的,为了解决上述2个问题,只能通过自定义KeyGenerator解决
自定义Key生成器CacheKeyGenerator:源码:
package com.zking.ssm.redis;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.util.ClassUtils;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
@Slf4j
public class CacheKeyGenerator implements KeyGenerator {
// custom cache key
public static final int NO_PARAM_KEY = 0;
public static final int NULL_PARAM_KEY = 53;
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder key = new StringBuilder();
key.append(target.getClass().getSimpleName()).append(".").append(method.getName()).append(":");
if (params.length == 0) {
key.append(NO_PARAM_KEY);
} else {
int count = 0;
for (Object param : params) {
if (0 != count) {//参数之间用,进行分隔
key.append(',');
}
if (param == null) {
key.append(NULL_PARAM_KEY);
} else if (ClassUtils.isPrimitiveArray(param.getClass())) {
int length = Array.getLength(param);
for (int i = 0; i < length; i++) {
key.append(Array.get(param, i));
key.append(',');
}
} else if (ClassUtils.isPrimitiveOrWrapper(param.getClass()) || param instanceof String) {
key.append(param);
} else {//Java一定要重写hashCode和eqauls
key.append(param.hashCode());
}
count++;
}
}
String finalKey = key.toString();
log.debug("using cache key={}", finalKey);
return finalKey;
}
}
,另外此类使用非加密哈希算法MurmurHash
(源码46行: Hashing.murmur3_128().hashString),需要引入google guava项目,其pom如下:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>27.0.1-jre</version>
</dependency>
redis缓存的一些注意事项
只应将热数据放到缓存中
所有缓存信息都应设置过期时间
缓存过期时间应当分散以避免集中过期
缓存key应具备可读性
应避免不同业务出现同名缓存key
可对key进行适当的缩写以节省内存空间
选择合适的数据结构
确保写入缓存中的数据是完整且正确的
避免使用耗时较长的操作命令,如:keys *
Redis默认配置中操作耗时超过10ms即视为慢查询
一个key对应的数据不应过大
对于string类型,一个key对应的value大小应控制在10K以内,1K左右更优hash类型,不应超过5000行
避免缓存穿透
数据库中未查询到的数据,可在Redis中设置特殊标识,以避免因缓存中无数据而导致每次请求均达到数据库
缓存层不应抛出异常
缓存应有降级处理方案,缓存出了问题要能回源到数据库进行处理
可以进行适当的缓存预热
对于上线后可能会有大量读请求的应用,在上线之前可预先将数据写入缓存中
读的顺序是先缓存,后数据库;写的顺序是先数据库,后缓存
数据一致性问题
数据源发生变更时可能导致缓存中数据与数据源中数据不一致,应根据实际业务需求来选择适当的缓存更新策略:
主动更新:在数据源发生变更时同步更新缓存数据或将缓存数据过期。一致性高,维护成本较高。
被动删除:根据缓存设置的过期时间有Redis负责数据的过期删除。一致性较低,维护成本较低。