redis最佳实践

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优化

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、其实,不管是先更新缓存还是先更新数据库,都有可能造成脏数据,只是先更新库造成脏数据的可能心很小很小,比如:一个是读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放进去,所以,会造成缓存脏数据。

缓存穿透

业务系统想要查询的数据根本不存在,所以一定会查数据库,如果有人恶意攻击,制造不存在的数据查询就导致数据库压力很大,解决方案如下:

  1. 将返回数据是null也存入缓存,下次请求就不读库了;(对于空数据的key各不相同、key重复请求概率低的场景下一种方案更好)
  2. 避免缓存穿透,使用BloomFilter,当业务系统有查询请求的时候,首先去BloomFilter中查询该key是否存在。若不存在,则说明数据库中也不存在该数据,因此缓存都不要查了,直接返回null。(如果空数据的key数量有限、key重复请求概率较高,上一种方案跟简便)

缓存雪崩

如果缓存因某种原因发生了宕机,那么原本被缓存抵挡的海量查询请求就会像疯狗一样涌向数据库,解决方案如下:

  1. 使用缓存集群,保证缓存高可用;
  2. 通过 熔断、降级、限流三个手段来降低雪崩发生后的损失。(hystrix sentinel)
  3. 熔断是指 a调用b,b调用c c返回太慢,b把c熔断了返回给a异常,a收到了b返回的异常,这样a b 都受到保护
  4. 限流是指 a调用b,b的流量有突增,就不响应请求或响应的非常慢

缓存击穿(热点数据集中失效)

对于一些请求量极高的热点数据而言,一旦过了有效时间,此刻将会有大量请求落在数据库上,从而可能会导致数据库崩溃,解决方案如下:

  • 使用缓存自带的锁机制,当第一个数据库查询请求发起后,就将缓存中该数据上锁;此时到达缓存的其他查询请求将无法查询该字段,从而被阻塞等待;当第一个请求完成数据库查询,并将数据更新至缓存后,释放锁,但是,由于采用了互斥锁,其他请求将会阻塞等待,此时系统的吞吐量将会下降。这需要结合实际的业务考虑是否允许这么做。
  • 互斥锁可以避免某一个热点数据失效导致数据库崩溃的问题,而在实际业务中,往往会存在一批热点数据同时失效的场景,我们可以将他们的缓存失效时间错开。这样能够避免同时失效。如:在一个基础时间上加/减一个随机数,从而将这些缓存的失效时间错开。

缓存预热

  • 上线时加个接口,手动触发加载缓存,或者定时夜间跑批刷新缓存。
  • 预发布环境提前代客相关角色对缓存预热。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值