Redis实践-商户查询缓存
1.项目背景
承接上一篇文章,我们还是以黑马点评作为项目背景,看一下Redis缓存在项目中的真实应用,我们看一眼相关的表:
Redis作为缓存服务使用时,提供了高性能、高可用性和易扩展性的解决方案,这是因为Redis的数据基于内存存储访问,速度非常快,还支持多种数据结构,如字符串、列表、集合、哈希、有序集合等,这种多样性使得它可以适应各种类型的缓存需求,同时允许为存储的每个键设置生存时间(TTL),过了这个时间键会被自动删除,这是管理缓存数据,特别是对于有时间敏感性的数据非常有用。除此以外,Redis支持数据的自动分片和集群部署,使得它能够处理更大规模的数据集和客户端连接,支持主从复制,提供数据的热备份,以及故障转移和灾难恢复能力。我们以查询商铺信息为例,看看Redis缓存如何使用:
2.缓存更新策略
对于高一致性需求来说,主动更新策略是逃不掉的,常见的主动更新策略有三种:Cache Aside Pattern
、Read/Write Through Pattern
和Write Behind Caching Pattern
,后两种策略实现和管理复杂性较大,我们这里还是使用第一种策略,即更新数据库的同时更新缓存
。
3.缓存穿透
1 2 3 4 5 6 7 8 9 10 11
| @Transactional public Result updateShop(Shop shop) { if (shop.getId() == null) { return Result.fail("商铺ID不能为空!"); } updateById(shop); stringRedisTemplate.delete(RedisConstants.CACHE_SHOP_KEY + shop.getId()); return Result.ok(); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public Result queryById(Long id) { String jsonShop = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id); if (StrUtil.isNotBlank(jsonShop)) { return Result.ok(JSONUtil.toBean(jsonShop, Shop.class)); } if (jsonShop != null) { return Result.fail("商铺不存在!"); } Shop shop = getById(id); if (shop == null) { stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES); return Result.fail("商铺不存在!"); } stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES); return Result.ok(shop); }
|
布隆过滤器Redis实现
4.缓存雪崩
缓存雪崩的解决方案涉及到Redis集群部署和多级缓存,这些内容会在后面的Redis高级篇中学习。
5.热点Key
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| public Result queryByIdWithMutex(Long id) { String jsonShop = null; while (true) { jsonShop = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id); if (StrUtil.isNotBlank(jsonShop)) { return Result.ok(JSONUtil.toBean(jsonShop, Shop.class)); } if (jsonShop != null) { return Result.fail("商铺不存在!"); } try { if (!tryLock(RedisConstants.LOCK_SHOP_KEY + id)) { TimeUnit.SECONDS.sleep(1); continue; } Shop shop = getById(id); if (shop == null) { stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES); return Result.fail("商铺不存在!"); } stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES); return Result.ok(shop); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { unlock(RedisConstants.LOCK_SHOP_KEY + id); } } }
private boolean tryLock(String key) { Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", RedisConstants.LOCK_SHOP_TTL, TimeUnit.SECONDS); return BooleanUtil.isTrue(flag); }
private void unlock(String key) { stringRedisTemplate.delete(key); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| private static final ExecutorService CACHE_BUILD_EXECUTOR = Executors.newFixedThreadPool(10); public static final Long CACHE_SHOP_LOGIC_TTL = 10L; public class RedisData { private LocalDateTime expireTime; private Object data; }
public Result queryByIdWithLogicExpire(Long id) { String jsonRedisData = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id); if (StrUtil.isNotBlank(jsonRedisData)) { RedisData data = JSONUtil.toBean(jsonRedisData, RedisData.class); if (data.getExpireTime().isAfter(LocalDateTime.now())) { return Result.ok(JSONUtil.toBean((JSONObject) data.getData(), Shop.class)); } try { if (tryLock(RedisConstants.LOCK_SHOP_KEY + id)) { jsonRedisData = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id); if (StrUtil.isNotBlank(jsonRedisData)) { data = JSONUtil.toBean(jsonRedisData, RedisData.class); if (data.getExpireTime().isAfter(LocalDateTime.now())) { return Result.ok(JSONUtil.toBean((JSONObject) data.getData(), Shop.class)); } } if (jsonRedisData.equals("")) { return Result.fail("商铺不存在!"); } CACHE_BUILD_EXECUTOR.submit(() -> { saveShop2Redis(id, RedisConstants.CACHE_SHOP_LOGIC_TTL); }); } } finally { unlock(RedisConstants.LOCK_SHOP_KEY + id); } return Result.ok(JSONUtil.toBean((JSONObject) data.getData(), Shop.class)); } if (jsonRedisData != null) { return Result.fail("商铺不存在!"); } Shop shop = saveShop2Redis(id, RedisConstants.CACHE_SHOP_LOGIC_TTL); if (shop == null) { return Result.fail("商铺不存在!"); } return Result.ok(shop); }
public Shop saveShop2Redis(Long id, Long expireSecond) { log.info("缓存重建..."); Shop shop = getById(id); if (shop == null) { stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES); return shop; } RedisData data = new RedisData(); data.setExpireTime(LocalDateTime.now().plusSeconds(expireSecond)); data.setData(shop); stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(data)); return shop; }
|