Redis缓存击穿解决方案

  • 👋 我是反钉党,大三本科生
  • 👀 我的兴趣是游戏、计算机、旅游、饥荒
  • 🌱 我正在学习计算机
  • 💞️ 一起学习进步
  • 📫 邮箱:3217998214@qq.com
  • 😄 个人网站 http://47.96.86.223/

分享一张壁纸:

wallhaven-d6y12l

缓存击穿

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

解决方案

方案优点缺点
互斥锁没有额外的内存消耗 保证一致性 实现简单线程需要等待 性能影响
逻辑过期线程无需等待 性能好不保证一致性 有额外内存消耗 实现复杂
互斥锁实现

使用 redis 的 setnx 指令实现:

  • setnx lock 1

如果存在,输出1,不存在输出0。

获取锁:

private boolean tryLock(String key) {
	// 有效期一般是比业务的执行时间长一点即可
	Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 3, TimeUnit.SECONDS);
	return BooleanUtil.isTrue(flag);
}

释放锁:

private void unlock(String key) {
	stringRedisTemplate.delete(key);
}
    private Shop queryWithMutex(Long id) {
        // 1. 从缓存查询
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        String jsonStr = stringRedisTemplate.opsForValue().get(key);
        if (jsonStr != null) {
            if (jsonStr.isEmpty()) {
                return null;
            } else {
                return JSONUtil.toBean(jsonStr, Shop.class);
            }
        }
        // 2. 缓存中不存在,重建缓存
        Shop shop;
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        try {
            if (!isLock) {
                // 没有拿到锁,休眠一会,重新查询
                Thread.sleep(50);
                // 重新在缓存中查询数据
                return queryWithMutex(id);
            } else {
                // 缓存重建
                shop = getById(id);
                if (shop == null) {
                    stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
                } else {
                    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            unlock(lockKey);
        }
        return shop;
    }
逻辑过期

定义一个新的Redis存储对象,加上逻辑过期时间字段:

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

这个方案需要进行缓存预热,要提前把数据加入到缓存里面,上代码:

	private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    // 逻辑过期解决缓存穿透
    public Shop queryWithLogicalExpire(Long id) {
        // 1. 从缓存查询
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        // 2. 查询是否存在
        String jsonStr = stringRedisTemplate.opsForValue().get(key);
        // 3. 命中,需要判断过期时间
        LocalDateTime now = LocalDateTime.now();
        RedisData redisData = JSONUtil.toBean(jsonStr, RedisData.class);
        // 4. 如果过期,获取互斥锁开启新线程执行缓存重建
        if (redisData.getExpireTime().isBefore(now)) {
            // 5. 执行缓存重建
            String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
            boolean isLock = tryLock(lockKey);
            if (isLock) {
                // 开启新线程
                CACHE_REBUILD_EXECUTOR.submit(() -> {
                    try {
                        // 缓存重建
                        rebuildCache(id, now);
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    } finally {
                        unlock(lockKey);
                    }
                });
            }
        }
        // 6. 不管有没有过期,都返回旧的数据
        return JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
    }

    public void rebuildCache(Long id, LocalDateTime now) {
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        Shop shop = getById(id);
        if (shop == null) {
            stringRedisTemplate.opsForValue().set(key,"", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
        } else {
            RedisData redisData = new RedisData();
            redisData.setExpireTime(now.plusSeconds(RedisConstants.CACHE_SHOP_TTL));
            redisData.setData(shop);
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
        }
    }
缓存工具类封装

主要解决缓存穿透、缓存击穿问题,使用双重检查锁定,锁的实现可以改成分布式锁,这里是简单的实现:

@Component
public class RedisUtil {
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    private final StringRedisTemplate stringRedisTemplate;

    @Value("${redis-util.key-prefix}")
    private String keyPrefix;

    @Value("${redis-util.null-value}")
    private String nullValue;

    @Value("${redis-util.null-value-expire}")
    private Long nullValueExpire;

    public RedisUtil(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public <T> void set(String key, T value) {
        key = keyPrefix + key;
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value));
    }

    public <T> void set(String key, T value, Long expireTime, TimeUnit timeUnit) {
        key = keyPrefix + key;
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), expireTime, timeUnit);
    }

    public <T> void setWithLogicalExpireTime(String key, T value, Long expireTime, TimeUnit timeUnit) {
        LocalDateTime now = LocalDateTime.now();
        key = keyPrefix + key;
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(now.plusSeconds(timeUnit.toSeconds(expireTime)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    public <T> T get(String key, Class<T> type, DbCallback<T> dbCallback) {
        key = keyPrefix + key;
        String json = stringRedisTemplate.opsForValue().get(key);
        if (json != null) {
            if (json.equals(nullValue)) {
                return null;
            } else {
                return JSONUtil.toBean(json, type);
            }
        }
        boolean isLock = tryLock(key);
        if (isLock) {
            try {
                json = stringRedisTemplate.opsForValue().get(key);
                if (json != null) {
                    if (json.equals(nullValue)) {
                        return null;
                    } else {
                        return JSONUtil.toBean(json, type);
                    }
                }
                T value = dbCallback.execute();
                if (value == null) {
                    set(key, nullValue, nullValueExpire, TimeUnit.SECONDS);
                } else {
                    set(key, value);
                }
                return value;
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                unLock(key);
            }
        } else {
            try {
                Thread.sleep(100);
                return get(key, type, dbCallback);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    public <T> T get(String key, Class<T> type, DbCallback<T> dbCallback, Long expireTime, TimeUnit timeUnit) {
        key = keyPrefix + key;
        String json = stringRedisTemplate.opsForValue().get(key);
        if (json != null) {
            if (json.equals(nullValue)) {
                return null;
            } else {
                return JSONUtil.toBean(json, type);
            }
        }
        boolean isLock = tryLock(key);
        if (isLock) {
            try {
                json = stringRedisTemplate.opsForValue().get(key);
                if (json != null) {
                    if (json.equals(nullValue)) {
                        return null;
                    } else {
                        return JSONUtil.toBean(json, type);
                    }
                }
                T value = dbCallback.execute();
                if (value == null) {
                    set(key, nullValue, nullValueExpire, TimeUnit.SECONDS);
                } else {
                    set(key, value, expireTime, timeUnit);
                }
                return value;
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                unLock(key);
            }
        } else {
            try {
                Thread.sleep(100);
                return get(key, type, dbCallback, expireTime, timeUnit);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    // 逻辑过期解决缓存击穿问题
    public <T> T getWithLogicalExpireTime(String key, Class<T> type, DbCallback<T> dbCallback, Long expireTime, TimeUnit timeUnit) {
        key = keyPrefix + key;
        LocalDateTime now = LocalDateTime.now();
        String json = stringRedisTemplate.opsForValue().get(key);
        if (json != null) {
            if (json.equals(nullValue)) {
                return null;
            } else {
                RedisData redisData = JSONUtil.toBean(json, RedisData.class);
                T data = JSONUtil.toBean((JSONObject) redisData.getData(), type);
                if (redisData.getExpireTime().isBefore(now)) {
                    boolean isLock = tryLock(key);
                    if (isLock) {
                        String finalKey = key;
                        json = stringRedisTemplate.opsForValue().get(finalKey);
                        if (json != null) {
                            if (json.equals(nullValue)) {
                                return null;
                            } else {
                                if (redisData.getExpireTime().isAfter(now)) {
                                    return JSONUtil.toBean(json, type);
                                }
                            }
                        }
                        CACHE_REBUILD_EXECUTOR.submit(() -> {
                            try {
                                T newData = dbCallback.execute();
                                if (newData == null) {
                                    set(finalKey, nullValue, nullValueExpire, TimeUnit.SECONDS);
                                } else {
                                    setWithLogicalExpireTime(finalKey, newData, expireTime, timeUnit);
                                }
                            } catch (Exception e) {
                                throw new RuntimeException(e);
                            } finally {
                                unLock(finalKey);
                            }
                        });
                    }
                }
                return data;
            }
        } else {
            boolean isLock = tryLock(key);
            if (isLock) {
                try {
                    json = stringRedisTemplate.opsForValue().get(key);
                    if (json != null) {
                        if (json.equals(nullValue)) {
                            return null;
                        } else {
                            return JSONUtil.toBean(json, type);
                        }
                    }
                    T data = dbCallback.execute();
                    if (data == null) {
                        set(key, nullValue, nullValueExpire, TimeUnit.SECONDS);
                    } else {
                        setWithLogicalExpireTime(key, data, expireTime, timeUnit);
                    }
                    return data;
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    unLock(key);
                }
            } else {
                try {
                    Thread.sleep(100);
                    return getWithLogicalExpireTime(key, type, dbCallback, expireTime, timeUnit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    public void del(String key) {
        key = keyPrefix + key;
        stringRedisTemplate.delete(key);
    }

    private boolean tryLock(String key) {
        String lockKey = keyPrefix + "lock:" + key;
        Boolean b = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "lock");
        return b != null && b;
    }

    private void unLock(String key) {
        String lockKey = keyPrefix + "lock:" + key;
        stringRedisTemplate.delete(lockKey);
    }

    public interface DbCallback<R> {
        R execute();
    }

    @Setter
    @Getter
    private static class RedisData {
        private Object data;
        private LocalDateTime expireTime;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值