逻辑过期解决缓存击穿问题

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

本问题主要针对热点key问题。

因为是热点,所以数据库中是一定存在的

缓存和数据库都不存在,那不是缓存击穿问题,是缓存穿透问题。这点要搞清楚。

特点:一致性不高,性能高,实现复杂

我自己画的图:

在这里插入图片描述

我们逻辑上设置一个过期时间expireTime,来判断数据是否过期。

于是,第一步

1.封装数据

@Data
public class RedisData {
    private LocalDateTime expireTime; 
    private Object data;
}

2.缓存预热
即提前在 真实的Redis缓存 中存入数据,这个数据在Redis内存中永不过期

这样,热点key在缓存中存储完毕。

我们人为的 在逻辑上,用 expireTime 来判断是否过期

没过期:很简单,在逻辑上 缓存中存在,直接返回

过期:较为复杂,在逻辑上 “缓存过期”了(即我们自定义对象RedisData中的expireTime过期了,并不是真实的Redis中的数据过期了)
尝试获取锁,若获取锁失败,直接返回旧数据!!!

​ 若成功获取锁,开启独立线程 Executors.newFixedThreadPool(10);

这个独立线程就相当于找个小弟,帮自己搞定麻烦事。
小弟做事,其他人通通闪开(其他线程返直接返回旧数据)
这个小弟(独立线程)做的事:缓存重建this.saveShop2Redis(id,20L);

小弟(独立线程)帮忙重建缓存,在小弟重建缓存完成之前,其它线程返回只能返回之前的数据。这也是 逻辑过期 出现不一致问题的关键点。

这里的缓存重建,是向我们封装的 RedisData对象 中进行操作,设置过期时间expireTime。实际上、物理上,Redis内存中数据是一直存在的!

最后在finally里面完成锁的释放

正式代码:

private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public Shop queryWithLogicalExpire( Long id ) {
    String key = CACHE_SHOP_KEY + id;
    // 1.从redis查询商铺缓存
    String json = stringRedisTemplate.opsForValue().get(key);
    // 2.判断是否存在
    if (StrUtil.isBlank(json)) {
        // 3.存在,直接返回
        return null;
    }
    // 4.命中,需要先把json反序列化为对象
    RedisData redisData = JSONUtil.toBean(json, RedisData.class);
    Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
    LocalDateTime expireTime = redisData.getExpireTime();
    // 5.判断是否过期
    if(expireTime.isAfter(LocalDateTime.now())) {
        // 5.1.未过期,直接返回店铺信息
        return shop;
    }
    // 5.2.已过期,需要缓存重建
    // 6.缓存重建
    // 6.1.获取互斥锁
    String lockKey = LOCK_SHOP_KEY + id;
    boolean isLock = tryLock(lockKey);
    // 6.2.判断是否获取锁成功
    if (isLock){
        CACHE_REBUILD_EXECUTOR.submit( ()->{

            try{
                //重建缓存
                this.saveShop2Redis(id,20L);
            }catch (Exception e){
                throw new RuntimeException(e);
            }finally {
                unlock(lockKey);
            }
        });
    }
    // 6.4.返回过期的商铺信息
    return shop;
}
  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
好的,下面是解决缓存击穿问题逻辑过期代码: ``` public class RedisUtil { private static final Logger logger = LoggerFactory.getLogger(RedisUtil.class); private static final Long SUCCESS = 1L; private static final String SET_IF_NOT_EXIST = "NX"; private static final String SET_WITH_EXPIRE_TIME = "PX"; private static final String LOCK_PREFIX = "redis_lock_"; private static final String OK = "OK"; private static final String UNLOCK_LUA; static { StringBuilder sb = new StringBuilder(); sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] "); sb.append("then "); sb.append(" return redis.call(\"del\",KEYS[1]) "); sb.append("else "); sb.append(" return 0 "); sb.append("end "); UNLOCK_LUA = sb.toString(); } private JedisPool jedisPool; public RedisUtil(JedisPool jedisPool) { this.jedisPool = jedisPool; } /** * 获取分布式锁 * * @param lockKey 锁 * @param requestId 请求标识 * @param expireTime 超期时间 * @return 是否获取成功 */ public boolean tryGetDistributedLock(String lockKey, String requestId, int expireTime) { Jedis jedis = jedisPool.getResource(); String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); jedis.close(); return OK.equals(result); } /** * 释放分布式锁 * * @param lockKey 锁 * @param requestId 请求标识 * @return 是否释放成功 */ public boolean releaseDistributedLock(String lockKey, String requestId) { Jedis jedis = jedisPool.getResource(); Object result = jedis.eval(UNLOCK_LUA, Collections.singletonList(lockKey), Collections.singletonList(requestId)); jedis.close(); return SUCCESS.equals(result); } /** * 获取缓存 * * @param key 缓存key * @return 缓存值 */ public String get(String key) { Jedis jedis = jedisPool.getResource(); String value = jedis.get(key); jedis.close(); return value; } /** * 设置缓存 * * @param key 缓存key * @param value 缓存值 * @return 是否设置成功 */ public boolean set(String key, String value) { Jedis jedis = jedisPool.getResource(); String result = jedis.set(key, value); jedis.close(); return OK.equals(result); } /** * 设置缓存并带过期时间 * * @param key 缓存key * @param value 缓存值 * @param expireTime 过期时间,单位秒 * @return 是否设置成功 */ public boolean setWithExpireTime(String key, String value, int expireTime) { Jedis jedis = jedisPool.getResource(); String result = jedis.setex(key, expireTime, value); jedis.close(); return OK.equals(result); } /** * 缓存逻辑过期 * * @param key 缓存key * @param expireSeconds 过期时间,单位秒 * @param getDataFunc 获取数据的函数 * @return 缓存值 */ public String getOrSetWithExpireLogic(String key, int expireSeconds, Supplier<String> getDataFunc) { String value = get(key); if (value == null) { // 获取分布式锁 String requestId = UUID.randomUUID().toString(); boolean lockResult = tryGetDistributedLock(LOCK_PREFIX + key, requestId, expireSeconds * 1000); if (lockResult) { // 获取数据 value = getDataFunc.get(); if (value != null) { // 设置缓存并带过期时间 setWithExpireTime(key, value, expireSeconds); logger.info("set cache success, key={}, expireSeconds={}", key, expireSeconds); } // 释放分布式锁 releaseDistributedLock(LOCK_PREFIX + key, requestId); } else { // 获取锁失败,等待一段时间后重试 try { Thread.sleep(100); } catch (InterruptedException e) { logger.error("线程等待异常", e); } // 递归调用自身 return getOrSetWithExpireLogic(key, expireSeconds, getDataFunc); } } return value; } } ``` 这段代码实现了一个缓存逻辑过期的功能。当缓存失效时,先获取分布式锁,然后再次检查缓存是否存在,如果不存在,则执行获取数据的函数,然后设置缓存并带过期时间,最后释放分布式锁。如果获取分布式锁失败,则等待一段时间后重试。这样可以避免缓存击穿问题
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值