spring-boot整合jedis与redission客户端
- 整合pom文件省略
- 配置文件
#redis缓存配置
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database=0
spring.redis.password=
spring.redis.timeout=10000
spring.redis.jedis.pool.max-active=1000
spring.redis.jedis.pool.max-idle=500
spring.redis.jedis.pool.min-idle=0
spring.redis.jedis.pool.max-wait=10000
- jedis配置类
@Configuration
public class MyJedisConfig extends CachingConfigurerSupport {
private Logger logger = LoggerFactory.getLogger(MyJedisConfig.class);
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.timeout}")
private int timeout;
@Value("${spring.redis.jedis.pool.max-active}")
private int maxActive;
@Value("${spring.redis.jedis.pool.max-idle}")
private int maxIdle;
@Value("${spring.redis.jedis.pool.min-idle}")
private int minIdle;
@Value("${spring.redis.jedis.pool.max-wait}")
private long maxWaitMillis;
@Bean
public JedisPool redisPoolFactory(){
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(maxIdle);
jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
jedisPoolConfig.setMaxTotal(maxActive);
jedisPoolConfig.setMinIdle(minIdle);
JedisPool jedisPool = new JedisPool(jedisPoolConfig,host,port,timeout,null);
logger.info("JedisPool注入成功!");
logger.info("redis地址:" + host + ":" + port);
return jedisPool;
}
}
- redission配置类
@Configuration
//CacheManager只要通过@EnableCaching注释启用缓存支持,Spring Boot将根据实现自动配置适当的配置。
//如果您使用的缓存基础结构与不是基于接口的bean,请确保启用该proxyTargetClass属性
@EnableCaching(proxyTargetClass = true)
@ImportResource("classpath:redisson.xml")
public class MyRedissionConfig {
@Resource
private RedissonClient redissonClient;
@Bean
CacheManager cacheManager() {
Map<String, CacheConfig> config = new HashMap<>();
CacheConfig cacheConfig = new CacheConfig();
cacheConfig.setTTL(day(1));
config.put(CacheConstants.DEFAULT, cacheConfig);
return new RedissonSpringCacheManager(redissonClient, config);
}
@Bean("myKeyGenerator")
//自定义缓存key生成器,若要使用,再注解中加入keyGenerator参数=myKeyGenerator即可
public KeyGenerator keyGenerator(){
return new KeyGenerator(){
@Override
public Object generate(Object target, Method method, Object... params) {
return method.getName()+"["+ Arrays.asList(params).toString()+"]";
}
};
}
private int hour(int hour) {
return hour * 60 * 60 * 1000;
}
private int day(int day) {
return day * 24 * 60 * 60 * 1000;
}
}
- redission.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:redisson="http://redisson.org/schema/redisson"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://redisson.org/schema/redisson http://redisson.org/schema/redisson/redisson.xsd">
<beans>
<redisson:client id="redissonClient" name="redissonClient" threads="0" netty-threads="2">
<redisson:single-server idle-connection-timeout="300000" connection-minimum-idle-size="0" subscription-connection-minimum-idle-size="0" connect-timeout="100"
timeout="100" retry-attempts="1" retry-interval="100" address="redis://127.0.0.1:6379" database="3" />
</redisson:client>
</beans>
</beans>
spring-cache的日常用法
- 三个常用注解
/**
* 批量获取日志信息
*
* @return
*/
@Override
//只缓存批量查询的第一批数据
@Cacheable(cacheNames = CacheConstants.DEFAULT, key = "'getBatchLog-' + #id + '-' + #limit", condition = "#id == 0", unless = "#result.size() == 0")
public List<SysLog> getBatchLog(Long id, Integer limit) {
return sysLogMapper.selectLogBatch(id, limit);
}
/**
* 批量获取访问信息
*
* @return
*/
@Override
//先查库再将结果放入缓存
//@CachePut(cacheNames = CacheConstants.DEFAULT, key = "'getBatchView-' + #id + '-' + #limit")
//先查库再将结果从缓存删除 beforeInvocation = true表示先删除缓存再查库
//@CacheEvict(cacheNames = CacheConstants.DEFAULT, key = "'getBatchView-' + #id + '-' + #limit")
public List<SysView> getBatchView(Long id, Integer limit) {
return sysViewMapper.selectViewBatch(id, limit);
}
spring-cache优化
如果上图中的入参是一个list ,返回list那这个缓存key怎么设计呢?这种传入的业务参数是一个集合类型,我们需要把业务参数返回的业务对象缓存在redis,应该把集合中的元素一一缓存,而不是缓存集合,因为每一个集合元素对应一条业务数据,但是问题又来了,当我们以后获取这个集合的时候,就需要遍历整个集合,每个元素都请求缓存获取结果再封装成集合结果返回,就存在网络io的性能问题,那么如何一次请求redis获取多个返回结果来优化网络io的时间呢?利用缓存的mget或者pipline能力批量一次性取多个。spring-cache基本可以满足常规需求啦,但这个时候spring-cache注解就不能满足我们的要求咯。
postCache的封装
- 维护一个redis缓存,创建一个切面,每次调用接口之前,执行这个切面类,完成业务接口的降级功能
@Aspect
@Component
@Slf4j
public class PostCacheAspect {
@Resource
private JedisProxy jedisProxy;
@Resource
private ThreadPoolUtil threadPoolUtil;
private static final Integer oneDaySeconds = 24 * 3600;
@Around("@annotation(PostCache)")
public Object around(ProceedingJoinPoint pjp) {
Object[] args = pjp.getArgs();
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
String key;
Object value;
try {
value = pjp.proceed(args);
processResultAsync(method, args, value);
} catch (Throwable t) { //业务逻辑未正常执行成功,去取缓存
String errorMsg = "interface: " + method.getDeclaringClass().getSimpleName() + ", method: " + method.getName();
try {
Pair<String, Integer> keyAndTTL = getKeyAndTTL(method, args);
key = keyAndTTL.getLeft();
errorMsg = errorMsg + ", key: " + key;
log.warn("invoke method error, failback triggered. " + errorMsg, t);
value = jedisProxy.get(key);
} catch (Exception e) { //缓存未取成功抛出降级异常
log.error("invoke method error, get Redis post-cache error. " + errorMsg, e);
throw new BackupCacheRedisException(errorMsg, e);
}
if(value instanceof NonExistRedisObject) {
log.error("invoke method error, get Redis post-cache key missed. " + errorMsg);
throw new BackupCacheKeyMissException(errorMsg, t);
}
log.warn("invoke method error, failback succeed. " + errorMsg + ", value:" + value);
return value; //取缓存
}
return value;
}
private Pair<String, Integer> getKeyAndTTL(Method method, Object[] args) {
Class<?> declaringClass = method.getDeclaringClass();
PostCacheConfig cacheConfigAnno = declaringClass.getAnnotation(PostCacheConfig.class);
String prefix = StringUtils.EMPTY;
int expire = oneDaySeconds;
if (cacheConfigAnno != null) {
prefix = cacheConfigAnno.prefix();
expire = cacheConfigAnno.expire();
}
String key = getKey(prefix, method.getDeclaringClass().getName(), method.getName(), args);
PostCache postCacheAnno = method.getAnnotation(PostCache.class);
if (postCacheAnno != null && postCacheAnno.expire() > 0) {
expire = postCacheAnno.expire();
}
return Pair.of(key, expire);
}
private String getKey(String prefix, String className, String methodName, Object[] args) {
String argStr = SimpleKeyGenerator.generateKey(args).toString();
return Joiner.on(":").join(prefix, className, methodName, argStr);
}
private void processResultAsync(Method method, Object[] args, Object value) {
ThreadPoolExecutor executor = threadPoolUtil.getCustomExecutorService();
executor.submit(new Runnable() {
@Override
public void run() {
Pair<String, Integer> keyAndTTL = null;
try {
keyAndTTL = PostCacheAspect.this.getKeyAndTTL(method, args);
String key = keyAndTTL.getLeft();
int expire = keyAndTTL.getRight();
jedisProxy.put(key, value, expire);
} catch (Exception ex) { //如刷新备用缓存失败了,则清除原有缓存,避免脏数据
log.error("put cache failed. key:" + (keyAndTTL == null ? "" : keyAndTTL.getLeft()), ex);
}
}
});
}
}
缓存一致性
- 如何考虑缓存一致性问题
1、同步更新:更新数据库后更新缓存;
2、异步更新:监听从库binlog更新缓存;
3、缓存过期时间兜底。 - Cache Miss的时候数据回源,为什么从主库读
1、Cache Miss的频率不是很高,即使从主库读也不会对主库造成太大的压力;
2、主库数据更加准确,如果这时主库正在向从库中同步数据,从从库中读取数据的话可能会使数据不是最新的,造成缓存脏数据;
3、其实,不管是回源主库还是回源从库,都有可能造成脏数据,只是回源主库造成脏数据的可能性很小很小,例如:主库正在向从库同步数据,这时一个进程Cache Miss,读主库,更新缓存,此时又来一个进程更新数据,清理缓存了,而在这之后最开始的从库更新才完成,收到从库binlog,再次更新成了第二个进程更新前的数据,就是脏数据了。 - 异步清理缓存,为什么监听从库binlog
1、异步清理缓存的操作如果相对比较频繁的话,从主库读取的话,会给主库增加压力;
2、我们读数据一般是从从库,否则也失去了主从意义,如果我们监听主库binlog,当我们监听到主库binlog清理缓存,这时候从库可能没有更新成功,这个时候就会缓存从库的脏数据。 - 数据库事务提交&缓存数据更新,两者谁先执行谁后执行
1、Cache Aside Pattern是我们最常用的模式,需要先提交事务更新库,再更新缓存;
2、如果先更新缓存:假设两个并发操作,一个是更新操作,另一个是查询操作,更新操作如果先清理缓存,查询操作没有命中缓存,把数据读出来后放到缓存中,然后更新操作更新了数据库。于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的,而且还一直这样脏下去了;
3、其实,不管是先更新缓存还是先更新数据库,都有可能造成脏数据,只是先更新库造成脏数据的可能心很小很小,比如:一个是读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放进去,所以,会造成缓存脏数据。
缓存穿透
业务系统想要查询的数据根本不存在,所以一定会查数据库,如果有人恶意攻击,制造不存在的数据查询就导致数据库压力很大,解决方案如下:
- 将返回数据是null也存入缓存,下次请求就不读库了;(对于空数据的key各不相同、key重复请求概率低的场景下一种方案更好)
- 避免缓存穿透,使用BloomFilter,当业务系统有查询请求的时候,首先去BloomFilter中查询该key是否存在。若不存在,则说明数据库中也不存在该数据,因此缓存都不要查了,直接返回null。(如果空数据的key数量有限、key重复请求概率较高,上一种方案跟简便)
缓存雪崩
如果缓存因某种原因发生了宕机,那么原本被缓存抵挡的海量查询请求就会像疯狗一样涌向数据库,解决方案如下:
- 使用缓存集群,保证缓存高可用;
- 通过 熔断、降级、限流三个手段来降低雪崩发生后的损失。(hystrix sentinel)
- 熔断是指 a调用b,b调用c c返回太慢,b把c熔断了返回给a异常,a收到了b返回的异常,这样a b 都受到保护
- 限流是指 a调用b,b的流量有突增,就不响应请求或响应的非常慢
缓存击穿(热点数据集中失效)
对于一些请求量极高的热点数据而言,一旦过了有效时间,此刻将会有大量请求落在数据库上,从而可能会导致数据库崩溃,解决方案如下:
- 使用缓存自带的锁机制,当第一个数据库查询请求发起后,就将缓存中该数据上锁;此时到达缓存的其他查询请求将无法查询该字段,从而被阻塞等待;当第一个请求完成数据库查询,并将数据更新至缓存后,释放锁,但是,由于采用了互斥锁,其他请求将会阻塞等待,此时系统的吞吐量将会下降。这需要结合实际的业务考虑是否允许这么做。
- 互斥锁可以避免某一个热点数据失效导致数据库崩溃的问题,而在实际业务中,往往会存在一批热点数据同时失效的场景,我们可以将他们的缓存失效时间错开。这样能够避免同时失效。如:在一个基础时间上加/减一个随机数,从而将这些缓存的失效时间错开。
缓存预热
- 上线时加个接口,手动触发加载缓存,或者定时夜间跑批刷新缓存。
- 预发布环境提前代客相关角色对缓存预热。