摘要
本文为如何应对redis缓存的击穿、穿透和雪崩系列文章的第一篇文章,主要介绍如何在上述场景下将防止击穿、穿透、和雪峰应用到实际开发中。
本文为如何应对redis缓存的击穿、穿透和雪崩系列文章的第一篇文章,主要介绍如何在上述场景下将防止击穿、穿透、和雪峰应用到实际开发中。
本文假设要求出一个数据列表接口,需求如下:
1、需要查询很多数据
2、热门数据可能会在一段时间后变得不再热门
3、一部分数据可能在一段时间后变成热门数据
1、我们知道,在大量QPS下,如果所有的数据都直接从数据库中查询,那磁盘IO产生的影响将会是影响接口性能的一个极大的因素,因此我们在考虑查询数据的时候,对于经常需要查询的数据会做成缓存。
2、考虑到数据经常过一段时间以后会很久不再使用,或者数据会突然被经常访问,那么我们在设计这个接口的时候就需要考虑到那些数据适合在缓存中存活,因此我们需要设置一个缓存的失效时间(当然需要考虑更新机制)
3、假如查询缓存没有查询到,那么势必需要查询数据库,并将数据库查询到的数据放在缓存中,以备下次使用
基于以上需求和分析,我们设计的缓存接口应该具有如下的功能:
1、支持批量传入KEY,返回结果为Map<key,value>
2、如果缓存没有查询到,支持调用者通过查询数据库并缓存到缓存中,这个时候我们这个接口就需要支持调用者个性化查询数据库并将回传到接口中
3、如果缓存中查询到,那么将会给这个数据续命
4、这个接口必须是通用性很强的接口,不需要关注2中的用户个性化的逻辑
基于如上分析,我们可以将这个接口设计为如下:
/** * 根据key列表查询redis,并将redis内容转为map输出 * @param keys 要拼接的key列表(比如一批数据的ID) * @param expireSecond 超时时间 * @param clazz 接收数据的对象 * @param fromKey 通用的redis ke模板(redis的key模板,可以将比如blog_redis_%d填充上面的key参数) * @param find 当缓存没有查询到时查询本地数据库的方法(用户的自定义方法需要有一个List参数,下文有讲) * @param <K> 拼接的key类型 * @param <V> 接收的对象类型 * @return */ <K, V> Map<K, V> getMapDataByKeys(List<K> keys, int expireSecond, Class<V> clazz, Function<K, String> fromKey, Function<List<K>, Map<K, V>> find);
上面接口的实现方法如下:
@Override public <K, V> Map<K, V> getMapDataByKeys(List<K> keys, int expireSecond, Class<V> clazz, Function<K, String> fromKey, Function<List<K>, Map<K, V>> find) { if (keys == null || keys.size() == 0) { return null; } List<String> cacheKeys = keys.stream().map(fromKey).collect(Collectors.toList()); List<V> values = mget(cacheKeys, clazz); Map<K, V> map = new LinkedHashMap<>(); List<K> notExists = new ArrayList<>(keys.size()); for (int i = 0; i < keys.size(); i++) { if (values.get(i) == null) { notExists.add(keys.get(i)); } else { map.put(keys.get(i), values.get(i)); } } if (notExists.size() == 0) { return map; } Map<K, V> kvMap = find.apply(keys); mset(kvMap, fromKey, expireSecond); if (!CollectionUtils.isEmpty(kvMap)) { Iterator<Map.Entry<K, V>> iterator = kvMap.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<K, V> next = iterator.next(); map.put(next.getKey(), next.getValue()); } } return map; } /** * 从缓存中查询数据 * @param keys key列表 * @param clazz 接收数据序列化对象 * @param <V> 对象类型 * @return */ public <V> List<V> mget(List<String> keys, Class<V> clazz) { if (CollectionUtils.isEmpty(keys)) { return Lists.newArrayList(); } List<Object> objects = redisTemplate.opsForValue().multiGet(keys); if (CollectionUtils.isEmpty(objects)) { return Lists.newArrayList(); } return objects.stream().map(e -> { return clazz.cast(e); }).collect(Collectors.toList()); } /** * 将数据查询到的数据缓存到redis中 * @param data 数据库查询到的数据 * @param fromKey 通用的redis ke模板(redis的key模板,可以将比如blog_redis_%d填充上面的key参数) * @param expireSecond 失效时间 * @param <K> key类型 * @param <V> 返回值类型 */ private <K, V> void mset(Map<K, V> data, Function<K, String> fromKey, int expireSecond) { if (CollectionUtils.isEmpty(data)) { return; } LinkedHashMap<String, V> paramMap = new LinkedHashMap<>(); List<String> keys = Lists.newArrayList(); Iterator<Map.Entry<K, V>> iterator = data.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<K, V> next = iterator.next(); String keyString = fromKey.apply(next.getKey()); paramMap.put(keyString, next.getValue()); keys.add(keyString); } redisTemplate.opsForValue().multiSet(paramMap); if (expireSecond > 0) { expire(keys, expireSecond); } } /** * 指定缓存失效时间 * * @param keys 键集合 * @param time 时间(秒) */ public boolean expire(List<String> keys, long time) { try { if (time > 0 && !CollectionUtils.isEmpty(keys)) { keys.stream().forEach(item -> { redisTemplate.expire(item, time, TimeUnit.SECONDS); }); } return true; } catch (Exception e) { e.printStackTrace(); return false; } }
有了上面的公共接口,我们就写一个demo来模拟调用下这个接口,看是否能够实现我们的需求
假设我们有段逻辑需要根据用户的id列表查询用户的信息,这个用户的id列表为
List<Integer> userIds = Lists.newArrayList(1, 2, 3, 4, 5);
我们在逻辑中可以这样调用
Map<Integer, PovUserCacheModel> userInfoMap = redisCacheService.getMapDataByKeys(userIds, 60, PovUserCacheModel.class, this::formatObjKey, userService::getUserInfoByIds);
其中redisCacheService指的是4.1中公共接口所在的service类
其中,formatObjKey方法如下:
@Slf4j @SpringBootTest public class TestRedis { @Autowired private RedisComponent redisComponent; @Autowired private RedisCacheService redisCacheService; @Autowired private UserService userService; public static final String REDIS_KEY_TEMP = "user_info_%d"; @Test public void testRedis() { List<Integer> userIds = Lists.newArrayList(1, 2, 3, 4, 5); Map<Integer, PovUserCacheModel> userInfoMap = redisCacheService.getMapDataByKeys(userIds, 60, PovUserCacheModel.class, this::formatObjKey, userService::getUserInfoByIds); log.info("缓存查询到的数据为:{}", JSONObject.toJSONString(userInfoMap)); } public String formatObjKey(Object keyVal) { return String.format(REDIS_KEY_TEMP, keyVal); } }
其中,UserSerivce接口如下(入参中的userIds指的是缓存中不存在的userId列表,需要查询数据库,自己实现即可,此处不再写实现逻辑):
public interface UserService { Map<Integer, PovUserCacheModel> getUserInfoByIds(List<Integer> userIds); }
通过以上方法,即可实现应对redis缓存的击穿、穿透和雪崩时,根据批量KEY将查询结果返回Map对象设计实现