springboot 自定义缓存 之Redis

概述

缓存在springboot项目中很常见,分布式项目中最常见的缓存机制就是通过redis缓存mybatis的查询数据,如下示例代码:

@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {

	@Bean
	public CacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
		RedisSerializationContext.SerializationPair serializationPair =
				RedisSerializationContext.SerializationPair.fromSerializer(getRedisSerializer());
		RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
				.entryTtl(Duration.ofSeconds(30))
				.serializeValuesWith(serializationPair);
		return RedisCacheManager
				.builder(RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory))
				.cacheDefaults(redisCacheConfiguration).build();
	}

  	private RedisSerializer<Object> getRedisSerializer(){
		return new GenericFastJsonRedisSerializer();
	}

}
public interface UserMapper {

    @Cacheable(cacheNames = "User:Id",key="#p0")
    public User findById(@Param("id") Integer id);
}

上述代码的作用,是在调用findById方法时优先查询redis中的缓存数据。如果redis对应缓存不存在,则请求mysql查询数据,并将数据缓存到redis中,设置缓存的过期时间为30秒。

问题
示例代码简单明了,但是有两个问题:

当redis连接出现异常时,调用findById方法会抛出异常影响到正常的业务流程;
扩展性差,不能实现多层缓存,无法灵活切换多种缓存中间件(在@Cacheable中指定cacheManager只能实现一个方法固定使用一种缓存机制);
CacheErrorHandler
缓存仅仅是为了业务更快地查询而存在的,如果因为缓存操作失败导致正常的业务流程失败,有点得不偿失了。因此需要开发者自定义CacheErrorHandler处理缓存读写的异常。

/**
 * 当缓存读写异常时,忽略异常
 */
public class IgnoreExceptionCacheErrorHandler implements CacheErrorHandler {

	private static final Logger log = LoggerFactory.getLogger(IgnoreExceptionCacheErrorHandler.class);

	@Override
	public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
		log.error(exception.getMessage(), exception);
	}

	@Override
	public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) {
		log.error(exception.getMessage(), exception);
	}

	@Override
	public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
		log.error(exception.getMessage(), exception);
	}

	@Override
	public void handleCacheClearError(RuntimeException exception, Cache cache) {
		log.error(exception.getMessage(), exception);
	}
}
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {

    /**
     * 添加自定义缓存异常处理
     * 当缓存读写异常时,忽略异常
     */
    @Override
    public CacheErrorHandler errorHandler() {
        return new IgnoreExceptionCacheErrorHandler();
    }
    
	@Bean
	public CacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
		RedisSerializationContext.SerializationPair serializationPair =
				RedisSerializationContext.SerializationPair.fromSerializer(getRedisSerializer());
		RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
				.entryTtl(Duration.ofSeconds(30))
				.serializeValuesWith(serializationPair);
		return RedisCacheManager
				.builder(RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory))
				.cacheDefaults(redisCacheConfiguration).build();
	}

  	private RedisSerializer<Object> getRedisSerializer(){
		return new GenericFastJsonRedisSerializer();
	}

}

缓存读写发生了异常,如果是读取redis异常,上述代码会导致调用findById读取缓存的值为空,从而继续从mysql读取数据,对业务没有影响。但是如果请求量很大就会出现缓存雪崩的问题,大量的查询请求发送到mysql导致mysql负载过大而阻塞甚至宕机,建议使用多层缓存兜底。

如果缓存写发生了异常,就可能导致mysql的数据和redis缓存的数据不一致的问题。为了解决该问题,需要继续扩展CacheErrorHandler的handleCachePutError和handleCacheEvictError方法,思路就是将redis写操作失败的key保存下来,通过重试任务删除这些key对应的redis缓存解决mysql数据与redis缓存数据不一致的问题。

CacheResolver
开发者可以通过自定义CacheResolver实现动态选择CacheManager,如下通过代码实现对findById调用时使用多种缓存机制:优先从堆内存读取缓存,堆内存缓存不存在时再从redis读取缓存,redis缓存不存在时最后从mysql读取数据,并将读取到的数据依次写到redis和堆内存中。

public class CustomCacheResolver implements CacheResolver, InitializingBean {

    @Nullable
    private List<CacheManager> cacheManagerList;

    public CustomCacheResolver(){
    }
    public CustomCacheResolver(List<CacheManager> cacheManagerList){
        this.cacheManagerList = cacheManagerList;
    }

    public void setCacheManagerList(@Nullable List<CacheManager> cacheManagerList) {
        this.cacheManagerList = cacheManagerList;
    }
    public List<CacheManager> getCacheManagerList() {
        return cacheManagerList;
    }

    @Override
    public void afterPropertiesSet()  {
        Assert.notNull(this.cacheManagerList, "CacheManager is required");
    }

    @Override
    public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) {
        Collection<String> cacheNames = getCacheNames(context);
        if (cacheNames == null) {
            return Collections.emptyList();
        }
        Collection<Cache> result = new ArrayList<>();
        for(CacheManager cacheManager : getCacheManagerList()){
            for (String cacheName : cacheNames) {
                Cache cache = cacheManager.getCache(cacheName);
                if (cache == null) {
                    throw new IllegalArgumentException("Cannot find cache named '" +
                            cacheName + "' for " + context.getOperation());
                }
                result.add(cache);
            }
        }
        return result;
    }

    private Collection<String> getCacheNames(CacheOperationInvocationContext<?> context){
        return context.getOperation().getCacheNames();
    }
}
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {

    @Autowired
    private RedisConnectionFactory connectionFactory;
    
    @Override
    public CacheResolver cacheResolver() {
        // 通过Guava实现的自定义堆内存缓存管理器
        CacheManager guavaCacheManager = new GuavaCacheManager();
        CacheManager redisCacheManager = redisCacheManager();
        List<CacheManager> list = new ArrayList<>();
        // 优先读取堆内存缓存
        list.add(concurrentMapCacheManager);
        // 堆内存缓存读取不到该key时再读取redis缓存
        list.add(redisCacheManager);
        return new CustomCacheResolver(list);
    }
    
    /**
     * 添加自定义缓存异常处理
     * 当缓存读写异常时,忽略异常
     */
    @Override
    public CacheErrorHandler errorHandler() {
        return new IgnoreExceptionCacheErrorHandler();
    }
    
	@Bean
	public CacheManager redisCacheManager() {
		RedisSerializationContext.SerializationPair serializationPair =
				RedisSerializationContext.SerializationPair.fromSerializer(getRedisSerializer());
		RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
				.entryTtl(Duration.ofSeconds(30))
				.serializeValuesWith(serializationPair);
		return RedisCacheManager
				.builder(RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory))
				.cacheDefaults(redisCacheConfiguration).build();
	}

  	private RedisSerializer<Object> getRedisSerializer(){
		return new GenericFastJsonRedisSerializer();
	}

}

通过自定义CacheResolver开发者可以实现更多的自定义功能,例如热点缓存自动升降级的场景:

项目大多数情况下只使用redis做缓存,当某些场景下个别数据成为了热数据,通过例如storm实时统计出热数据后,项目将这些热数据缓存到堆内存,缓解网络和redis的负载压力。

这种场景完全可以通过自定义CacheResolver来实现,storm实时统计出热数据,自定义的CacheResolver在调用resolveCaches选择CacheManager前,先判断此次读写的缓存key是否是热数据。如果是热数据则使用堆内存的CacheManager,否则使用redis的CacheManager。
————————————————
版权声明:本文为CSDN博主「祈雨v」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/sz85850597/article/details/89301331

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值