当前位置: 首页 > news >正文

Redis查询缓存

什么是缓存?

缓存是一种提高数据访问效率的技术,通过在内存中存储数据的副本来减少对数据库或其他慢速存储设备的频繁访问。缓存通常用于存储热点数据或计算代价高的结果,以加快响应速度。

添加Redis缓存有什么好处? 

Redis 基于内存存储,读写速度极快,相比于传统的磁盘存储方式,能够显著提高系统的响应速度。

缓存了高频访问的数据后,可以减少对数据库的访问次数,从而减轻数据库的负载。

简单的将数据存入Redis之后存在什么问题?

缓存一致性问题

数据库中的数据发生变化时,缓存中的数据可能没有及时更新,导致数据不一致。

解决办法:使用缓存更新策略,如主动更新(推荐)、延迟双删、或设置缓存失效时间。

缓存穿透

如果客户端频繁请求数据库中不存在的数据,而这些数据不会被缓存,最终所有请求都会直接打到数据库,增加负载。

解决办法:为不存在的数据设置一个短时间的空值缓存。

缓存雪崩

当大量缓存数据同时过期或 Redis 宕机时,所有请求涌向数据库,可能导致系统崩溃。

解决办法:为不同缓存设置不同的过期时间(分布式过期时间),并配置缓存服务的高可用性(如主从、集群)。

缓存击穿

某个热点数据突然失效时,大量请求直接打到数据库,可能导致数据库压力骤增。

解决办法:使用互斥锁机制,确保缓存重新加载时只有一个请求访问数据库。或者使用逻辑过期方式。这两种需要根据情况来选择。如果需要确保数据的一致性推荐使用互斥锁机制。如果短暂的数据不一致不要紧,追求的是访问速度,推荐使用逻辑过期方式

缓存更新策略

为什么读操作未命中Redis,读数据库存入Redis还需要设置超时时间呢?

通过设置超时时间,即使缓存更新失败,缓存数据会在过期时间后自动失效,重新加载最新数据,确保数据最终一致性。也就是兜底方案。

为什么写的操作是先写入数据库,再删除缓存呢? 

1. 避免读写竞争问题

如果先删除缓存再写数据库,可能出现以下情况:

  • 线程 A 删除缓存。
  • 线程 B 查询数据时发现缓存为空,去数据库查询旧数据,并将其重新写入缓存。
  • 线程 A 写入新的数据到数据库。

这样,缓存中保存的就会是旧数据,导致数据不一致。

为什么先写入数据库就不存在这种读写竞争问题?

因为对Redis的读写时间耗时很短在很短的时间内CPU的执行权被抢夺的概率很小,但是更新数据库的操作比较久,尤其涉及到多表查询更新的时候。被抢夺CPU执行权的概率比较大。

2. 确保缓存数据最终一致

先写入数据库再删除缓存的顺序,可以保证以下情况:

  • 数据库中总是有最新的数据。
  • 即使缓存被删除后有线程查询,触发缓存更新时,查询到的也是最新的数据。
package com.hmdp.service.impl;import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.util.concurrent.TimeUnit;import static com.hmdp.utils.RedisConstants.CACHE_SHOP_KEY;
import static com.hmdp.utils.RedisConstants.CACHE_SHOP_TTL;/*** <p>*  服务实现类* </p>** @author 虎哥* @since 2021-12-22*/
@Service
@RequiredArgsConstructor
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {final StringRedisTemplate stringRedisTemplate;/*** 实现Redis缓存店铺信息* @param id* @return*/@Overridepublic Result queryById(Long id) {// 1. 首先从Redis当中查询是否存在商铺// 存储对象可以使用HashMap 也可以使用string类型String shopStr = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);// 存在直接返回Redis中的信息if (StrUtil.isNotBlank(shopStr)) {// 将string类型转换为对象Shop shop = JSONUtil.toBean(shopStr, Shop.class);return Result.ok(shop);}// 不存在,则查询数据库Shop shop = getById(id);if (shop == null) {// 数据库不存在返回404return Result.fail("商铺不存在!");}// 数据库中存在商铺信息则将商铺信息存入Redis 并且设置超时时间作为兜底方案stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);// 返回商铺信息return Result.ok(shop);}@Override@Transactionalpublic Result updateShop(Shop shop) {Long id = shop.getId();if (id == null ){return Result.fail("店铺id不能为空!");}// 先更新数据库updateById(shop);// 删除缓存stringRedisTemplate.delete(CACHE_SHOP_KEY +id );return Result.ok();}
}

 缓存穿透(访问不存在的数据)

也就是说我们在之前的查询的代码上,还需要添加如果Redis当中存在值且不为空直接返回Redis中的数据。还需要判断Redis存储的数据是否wield空 如果为空直接返回错误信息(不需要再查询数据库,导致数据库崩溃)。如果Redis当中的值为null我们需要查询数据库 如果数据库查询不到,我们需要将这个不存在的数据保存在Redis,值为空(防止缓存击穿),并且设置TTL

判断 Redis 是否存储了空值

if (shopStr != null) 判断了 Redis 中的值是否为 ,如果为 "" 或空值标记,表示这是防止缓存穿透写入的特殊数据,直接返回错误信息。

数据库查询为空时缓存空值

使用 "" 或其他标记作为空值,写入 Redis,并设置一个较短的过期时间(例如 2 分钟)。这样可以避免频繁查询数据库,防止缓存穿透。

正常数据的缓存设置 TTL

如果数据库中有数据,写入 Redis 时设置长时间的 TTL(如 CACHE_SHOP_TTL),确保数据一致性。

更新数据后删除缓存

确保先更新数据库,再删除缓存,避免并发情况下的数据不一致问题。

package com.hmdp.service.impl;import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.util.concurrent.TimeUnit;import static com.hmdp.utils.RedisConstants.CACHE_SHOP_KEY;
import static com.hmdp.utils.RedisConstants.CACHE_SHOP_TTL;/*** <p>* 服务实现类* </p>** @author 虎哥* @since 2021-12-22*/
@Service
@RequiredArgsConstructor
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {final StringRedisTemplate stringRedisTemplate;/*** 实现Redis缓存店铺信息** @param id* @return*/@Overridepublic Result queryById(Long id) {// 1. 从Redis中查询商铺信息String shopStr = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);// 2. 如果Redis中存在值且不为空字符串,直接返回if (StrUtil.isNotBlank(shopStr)) {Shop shop = JSONUtil.toBean(shopStr, Shop.class);return Result.ok(shop);}// 3. 判断Redis中是否存储了空值,防止缓存穿透if (shopStr != null) {return Result.fail("商铺不存在!");}// 4. Redis中不存在数据,从数据库查询Shop shop = getById(id);// 5. 如果数据库中也不存在,则写入一个空值到Redis,并设置短TTL,防止缓存穿透if (shop == null) {stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", // 空值标记2, // 短时间TTL(如2分钟)TimeUnit.MINUTES);return Result.fail("商铺不存在!");}// 6. 如果数据库中存在,将商铺信息写入Redis,并设置TTLstringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL,TimeUnit.MINUTES);// 7. 返回商铺信息return Result.ok(shop);}/*** 更新商铺信息** @param shop* @return*/@Override@Transactionalpublic Result updateShop(Shop shop) {Long id = shop.getId();if (id == null) {return Result.fail("店铺id不能为空!");}// 1. 先更新数据库updateById(shop);// 2. 再删除缓存stringRedisTemplate.delete(CACHE_SHOP_KEY + id);return Result.ok();}
}

缓存雪崩 

 

缓存击穿问题:(数据过期同时有大量请求)

 缓存击穿问题发生在某些热点数据在缓存失效的瞬间,同时有大量请求涌入,从而导致这些请求直接访问数据库,可能会引发数据库压力过大甚至宕机的情况。

1. 给线程加锁(互斥锁)

原理:

通过对热点数据的访问加互斥锁(如分布式锁或本地锁),保证在缓存失效后,只有一个线程能去查询数据库并更新缓存,其余线程等待锁释放后从缓存读取数据。

优点:
  1. 简单易实现:逻辑清晰,只需引入锁机制即可。
  2. 保护数据库:有效防止多线程同时查询数据库,降低数据库压力。
  3. 通用性强:适用于所有数据场景,尤其是并发量高的热点数据。
缺点:
  1. 性能问题
    • 在高并发情况下,锁的排队等待会增加响应时间。
    • 如果锁竞争激烈,会导致线程阻塞。
  2. 单点问题
    • 如果锁是本地实现,可能会出现分布式环境中的一致性问题。
    • 使用分布式锁(如 Redis 分布式锁)会增加实现复杂度。
  3. 潜在死锁风险:如果锁机制设计不当,可能会导致死锁。

使用互斥锁的代码逻辑:

查询 Redis:

  • 首先查询 Redis 是否存在目标数据。
  • 如果命中,直接返回。
  • 如果未命中(Redis 中没有目标数据),进入下一步。
  • 尝试获取锁:

如果获取锁成功:

  1. 再次查询 Redis 是否有数据(防止其他线程已加载数据)。
  2. 如果 Redis 中仍然没有数据,查询数据库,加载数据到 Redis。
  3. 释放锁,返回数据。

如果未获取到锁:

  1. 等待一段时间后,重复尝试获取锁。
  2. 在每次尝试获取锁时,先查询 Redis 是否已存在数据,防止无意义的锁竞争。

为什么获取锁之后还需要再次检查Redis中是否存在数据?

核心原因: 避免重复查询数据库和重复写入 Redis,从而减少资源浪费。

多线程场景下的问题:

  • 假设线程 A 获取到锁并完成了缓存重建(从数据库查询数据并写入 Redis)。
  • 线程 B 在等待锁的过程中,其实线程 A 已经完成了数据的缓存。
  • 线程 B 获取到锁时,如果不再次检查 Redis,就会重复从数据库查询并覆盖 Redis 中的已有数据,导致不必要的开销和延迟

这个锁应该是什么样的锁?是平常的吗 

 使用 Redis 实现分布式锁

Redis 是实现分布式锁的常用工具。通过 SETNXset if not exists)命令以及锁的过期时间,可以实现高效且可靠的分布式锁。

  • 如果键不存在,SETNX 会创建这个键并返回成功(true)。
  • 如果键已经存在,SETNX 会直接返回失败(false)。
/*** 尝试获取锁* @param key 锁的唯一标识* @return 是否获取成功*/
private boolean tryLock(String key) {Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);return BooleanUtil.isTrue(success);
}/*** 释放锁* @param key 锁的唯一标识*/
private void unlock(String key) {stringRedisTemplate.delete(key);
}
@Service
@RequiredArgsConstructor
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {final StringRedisTemplate stringRedisTemplate;/*** 查询商铺信息,包含缓存击穿的解决方案*/@Overridepublic Result queryById(Long id) {// 通过互斥锁解决缓存击穿问题Shop shop = queryWithMutex(id);if (shop == null) {return Result.fail("店铺不存在!");}return Result.ok(shop);}/*** 互斥锁解决缓存击穿*/public Shop queryWithMutex(Long id) {String key = CACHE_SHOP_KEY + id;String lockKey = LOCK_SHOP_KEY + id;// 1. 查询缓存String shopJson = stringRedisTemplate.opsForValue().get(key);if (StrUtil.isNotBlank(shopJson)) {return JSONUtil.toBean(shopJson, Shop.class);}if (shopJson != null) {return null; // 空值}Shop shop = null;boolean isLockAcquired = false;try {// 2. 尝试获取锁while (!(isLockAcquired = tryLock(lockKey))) {Thread.sleep(50); // 未获取锁,等待并重试}// 3. 再次检查缓存,防止重复查询数据库String shopJsonAfterLock = stringRedisTemplate.opsForValue().get(key);if (StrUtil.isNotBlank(shopJsonAfterLock)) {return JSONUtil.toBean(shopJsonAfterLock, Shop.class);}if (shopJsonAfterLock != null) {return null; // 空值}// 4. 查询数据库shop = getById(id);if (shop == null) {// 数据库不存在,写入空值防止缓存穿透stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}// 5. 写入缓存stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);} catch (InterruptedException e) {Thread.currentThread().interrupt(); // 恢复线程中断状态throw new RuntimeException("线程被中断", e);} finally {// 6. 释放锁if (isLockAcquired) {unlock(lockKey);}}return shop;}/*** 获取锁*/private boolean tryLock(String key) {Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);return BooleanUtil.isTrue(success);}/*** 释放锁(防止误删其他线程的锁)*/private void unlock(String key) {stringRedisTemplate.delete(key);}/*** 更新商铺信息,并清除缓存*/@Override@Transactionalpublic Result updateShop(Shop shop) {Long id = shop.getId();if (id == null) {return Result.fail("店铺id不能为空!");}// 1. 更新数据库updateById(shop);// 2. 删除缓存stringRedisTemplate.delete(CACHE_SHOP_KEY + id);return Result.ok();}
}

2. 逻辑过期

原理:

将缓存数据设置为逻辑过期时间,同时保留旧值。当缓存过期时,请求仍然返回旧数据,同时后台异步更新缓存(通过任务队列或线程池刷新数据)。

优点:
  1. 高性能
    • 由于返回旧数据,请求不会直接打到数据库,避免了数据库的压力。
    • 不会阻塞用户线程,用户体验更好。
  2. 无锁机制
    • 通过异步任务更新缓存,避免了加锁的复杂性和性能问题。
  3. 支持高并发
    • 用户请求不会因缓存失效而阻塞。
缺点:
  1. 数据一致性问题
    • 在缓存逻辑过期的时间段内,可能返回的是旧数据,适合对一致性要求不高的场景。
  2. 实现复杂度高
    • 需要设计异步更新逻辑,增加系统复杂性。
    • 需要引入额外的定时任务或异步线程池处理刷新逻辑。
  3. 资源占用
    • 异步更新的任务可能会带来额外的资源消耗,尤其是在数据量较大时。

选择建议

  1. 互斥锁适合对数据一致性要求高的业务场景,如订单、库存等核心数据,且并发量较低。
  2. 逻辑过期更适合高并发、热点数据且对数据一致性要求不高的场景,如新闻热点、排行榜等。

逻辑过期方式解决问题的逻辑:

查询缓存数据

  • 通过Rediskey查询数据,结果可能是:
    • 缓存未命中:直接返回null
    • 缓存命中:继续处理逻辑。

判断逻辑过期

  • 未过期:直接返回缓存中的数据。
  • 已过期:需要重建缓存。
  • 重建缓存(防止缓存穿透)

获取分布式锁(互斥锁)。

  • 如果获取不到锁:
    • 直接返回旧的数据,避免阻塞等待。
  • 如果获取到锁:
    • 再次检查是否过期(双重检查锁机制),防止锁释放期间其他线程已更新数据。
    • 开启一个独立线程完成缓存重建,释放锁后返回旧数据。

缓存重建逻辑

  • 查询数据库获取最新数据。
  • 写入Redis,并设置逻辑过期时间。
  • 释放锁,确保其他线程可以继续执行。
public Shop queryWithLogicExpire(Long id){String key = CACHE_SHOP_KEY+id;// 1. 首先从Redis当中查询是否存在商铺// 存储对象可以使用HashMap 也可以使用string类型String shopStr = stringRedisTemplate.opsForValue().get(key);// Redis中不存在该信息if(StrUtil.isBlank(shopStr)){return null;}RedisData redisData = JSONUtil.toBean(shopStr, RedisData.class);
//        Shop data = (Shop) redisData.getData();Shop data = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);// 存在 需要判断缓存是否逻辑过期// 未过期 直接返回数据if (redisData.getExpireTime().isAfter(LocalDateTime.now())){return data;}// 过期 缓存重建// 尝试获取互斥锁String lockKey = LOCK_SHOP_KEY + id;if (tryLock(lockKey)) {//如果线程b检查完过期时间后,线程a刚好重建完成并释放锁,// 此时线程b可以拿到锁并再次重建,所以需要进行二次校验过期时间// 获取锁之后还需要再次检测Redis缓存是否过期 如果未过期就不需要再开启新的线程shopStr = stringRedisTemplate.opsForValue().get(key);RedisData redisDataAfterGetLock = JSONUtil.toBean(shopStr, RedisData.class);if (redisDataAfterGetLock.getExpireTime().isAfter(LocalDateTime.now())) {return JSONUtil.toBean((JSONObject) redisDataAfterGetLock.getData(),Shop.class);}//TODO 获取到了就开启独立的线程,CATHE_REBUILD_EXECUTOR.submit(()->{try {// 重建缓存this.saveShop2Redis(id,CACHE_SHOP_TTL);} catch (Exception e) {log.error("缓存重建失败", e);}finally {// 释放锁unlock(lockKey);}});}//  还是返回旧的数据return data;

为什么需要进行二次校验?

以下是可能的线程执行时间线:

  1. 线程A:发现缓存过期,获取锁,进入重建逻辑。
  2. 线程B:也发现缓存过期,但未获取到锁,进入等待状态。
  3. 线程A:完成缓存重建,更新Redis数据,释放锁。
  4. 线程B:获取到锁,继续执行(认为缓存仍然过期)。
    • 如果没有二次校验,线程B会再次重建缓存。
    • 如果有二次校验,线程B会发现缓存数据已经更新,不需要重复重建。

此时,如果线程A完成缓存重建并释放锁后,线程B再次获取锁,直接重建缓存,会造成:

  1. 重复的缓存重建(浪费资源)。
  2. 数据被多次写入Redis,增加Redis的负担。

二次校验通过在获取锁后,再次检查缓存数据是否过期(或已经被其他线程更新),可以避免这种重复操作。

为什么没有查到数据的时候直接返回null?

因为对于热点key来说,我们会先将数据存储到Redis当中,并且设置逻辑过期时间。如果根据key在Redis当中查询不到该数据,只能说明这个数据不在热点当中,直接返回空即可。

未命中说明:

  • key不属于热点数据或从未被缓存。
  • 逻辑过期方案主要针对热点数据的缓存维护,对非热点数据无需增加负担。

如何设置逻辑超时时间?

引入一个RedisData类,包含以下字段:

  • expireTime:逻辑过期时间(LocalDateTime类型)。
  • data:具体的业务数据对象(如Shop类)
package com.hmdp.utils;import lombok.Data;import java.time.LocalDateTime;@Data
public class RedisData {private LocalDateTime expireTime;private Object data;
}

缓存数据存储到Redis

RedisData redisData = new RedisData();
redisData.setExpireTime(LocalDateTime.now().plusSeconds(CACHE_SHOP_TTL));
redisData.setData(shop); // shop 是业务对象
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));


使用JSON序列化将RedisData对象保存到Redis中: 

封装成工具类 

 

set:普通的缓存设置方法。

setWithLogicExpire:设置逻辑过期缓存。

queryWithPassThrough:解决缓存穿透问题。

queryWithLogicExpire:解决缓存击穿问题(使用逻辑过期方案)。

需要注意两个点:

提高通用性:面向多个类封装工具包的设计原则

使用泛型:

当工具包需要支持多个类时,提高通用性是关键。可以通过以下方式实现:

1.1 使用泛型
  • 为什么使用泛型:不同的调用者可能会涉及不同的数据模型和返回类型。通过泛型,可以让工具方法适配任意类型的返回值,而不需要为每个类重复编写代码。
  • 如何使用泛型
    • 定义返回值类型 R:适配不同的对象类型(如 ShopUser 等)。
    • 定义主键类型 ID:适配不同的数据主键(如 LongString)。
public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit
) {// 逻辑与实现
}
  • R 代表返回的实体类类型,如 ShopUser 等。
  • ID 代表主键的类型,如 Long(数字型 ID)或 String(UUID)。

2. 如何根据调用者需求获取数据库数据

关键点:调用者可能有不同的需求,例如:

  1. 查询逻辑不同(按 ID、按名称等)。
  2. 数据返回类型不同(单个对象、列表等)。
  3. 数据库表不同。

解决方案: 使用 Function<ID, R> 获取数据

Function<ID, R> 的优势
  1. 灵活性:调用者可以动态传递 Lambda 表达式,实现灵活的查询逻辑。
  2. 解耦:工具类不需要依赖具体的服务或 DAO 实现,而是将查询逻辑交给调用者。
  3. 简单易用:调用者可以用 Lambda 或方法引用直接定义查询逻辑。
工具类的实现
public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type,Function<ID, R> dbFallback, Long time, TimeUnit unit
) {String key = keyPrefix + id;String json = stringRedisTemplate.opsForValue().get(key);// 判断缓存中是否有数据if (StrUtil.isNotBlank(json)) {return JSONUtil.toBean(json, type); // 缓存命中,返回数据}// 查询数据库R result = dbFallback.apply(id);if (result == null) {// 防止缓存穿透,缓存空值stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}// 数据库有数据,写入缓存this.set(key, result, time, unit);return result;
}
调用示例

使用 Lambda 表达式或方法引用定义数据库查询逻辑:

// Lambda 表达式
queryWithPassThrough("shop:", 1L, Shop.class,id -> shopService.findById(id),10L, TimeUnit.MINUTES
);// 方法引用
queryWithPassThrough("user:", "abc123", User.class,userService::findById,10L, TimeUnit.MINUTES
);
package com.hmdp.utils;import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.entity.Shop;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;import static com.hmdp.utils.RedisConstants.*;@Slf4j
@Component
@RequiredArgsConstructor
public class CacheClient {final StringRedisTemplate stringRedisTemplate;public  void set(String key, Object value, Long time, TimeUnit unit){stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);}public  void setWithLogicExpire(String key, Object value, Long time, TimeUnit unit){RedisData redisData = new RedisData();redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));redisData.setData(value);stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));}public <R,ID> R queryWithPassThrough(String keyPrefix, ID id,Class<R> type, Function<ID,R> dbFallback,Long time, TimeUnit unit){// 1. 首先从Redis当中查询是否存在商铺// 存储对象可以使用HashMap 也可以使用string类型String key = keyPrefix + id;String Json = stringRedisTemplate.opsForValue().get(key);// 存在直接返回Redis中的信息if (StrUtil.isNotBlank(Json)) {// 将string类型转换为对象return JSONUtil.toBean(Json,type);}// 判断命中的是否是空值if (Json != null) {// 是空值,返回一个错误信息return null;}// 不存在,则查询数据库R r =dbFallback.apply(id);if (r == null) {// 数据库不存在返回404// 要将空值写入Redis 防止缓存穿透stringRedisTemplate.opsForValue().set(keyPrefix +id,"",CACHE_NULL_TTL,TimeUnit.MINUTES);return null;}// 数据库中存在商铺信息则将商铺信息存入Redis 并且设置超时时间作为兜底方案this.set(key,r,time,unit);// 返回商铺信息return r;}// 线程池// 不知道怎么根据id查询数据库?那么就需要面向函数式接口,让调用者传递函数private static final ExecutorService CATHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);public <R,ID> R queryWithLogicExpire(String prefixKey, String PrefixLock,ID id,Class<R> type,Function<ID,R> dbFallback,  Long time, TimeUnit unit){String key = prefixKey+id;// 1. 首先从Redis当中查询是否存在商铺// 存储对象可以使用HashMap 也可以使用string类型String json = stringRedisTemplate.opsForValue().get(key);// Redis中不存在该信息if(StrUtil.isBlank(json)){return null;}RedisData redisData = JSONUtil.toBean(json, RedisData.class);
//        Shop data = (Shop) redisData.getData();R data = JSONUtil.toBean((JSONObject) redisData.getData(), type);// 存在 需要判断缓存是否逻辑过期// 未过期 直接返回数据if (redisData.getExpireTime().isAfter(LocalDateTime.now())){return data;}// 过期 缓存重建// 尝试获取互斥锁String lockKey = PrefixLock + id;if (tryLock(lockKey)) {//如果线程b检查完过期时间后,线程a刚好重建完成并释放锁,// 此时线程b可以拿到锁并再次重建,所以需要进行二次校验过期时间// 获取锁之后还需要再次检测Redis缓存是否过期 如果未过期就不需要再开启新的线程json = stringRedisTemplate.opsForValue().get(key);RedisData redisDataAfterGetLock = JSONUtil.toBean(json, RedisData.class);if (redisDataAfterGetLock.getExpireTime().isAfter(LocalDateTime.now())) {return JSONUtil.toBean((JSONObject) redisDataAfterGetLock.getData(),type);}//TODO 获取到了就开启独立的线程,CATHE_REBUILD_EXECUTOR.submit(()->{try {// 重建缓存// 查询数据库R r = dbFallback.apply(id);this.setWithLogicExpire(key,r,time,unit);} catch (Exception e) {log.error("缓存重建失败", e);}finally {// 释放锁unlock(lockKey);}});}//  还是返回旧的数据return data;}/*** 获取锁*/private boolean tryLock(String key) {Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);return BooleanUtil.isTrue(success);}/*** 释放锁(防止误删其他线程的锁)*/private void unlock(String key) {stringRedisTemplate.delete(key);}
}

相关文章:

Redis查询缓存

什么是缓存&#xff1f; 缓存是一种提高数据访问效率的技术&#xff0c;通过在内存中存储数据的副本来减少对数据库或其他慢速存储设备的频繁访问。缓存通常用于存储热点数据或计算代价高的结果&#xff0c;以加快响应速度。 添加Redis缓存有什么好处&#xff1f; Redis 基…...

双馈风电DFIG并网系统次转子侧变流器RSC抑制策略研究基于LADRC和重复控制的方法

风电装机容量的持续增长以及电力电子装置的大规模接入&#xff0c;导致电网强度降低&#xff0c;系 统运行特性发生深刻变化&#xff0c;严重威胁风电并网系统的安全稳定运行。因此本文以双馈风 电场经串补线路并网系统为研究对象&#xff0c;在深入分析双馈风电并网系统振荡…...

国产编辑器EverEdit - 使用技巧:变量重命名的一种简单替代方法

1 使用技巧&#xff1a;变量重命名的一种简单替代方法 1.1 应用场景 写过代码的都知道&#xff0c;经常添加功能的时候&#xff0c;是把别的地方的代码拷贝过来&#xff0c;改吧改吧&#xff0c;就能用了&#xff0c;改的过程中&#xff0c;就涉及到一个变量名的问题&#xff…...

使用SSH建立内网穿透,能够访问内网的web服务器

搞了一个晚上&#xff0c;终于建立了一个内网穿透。和AI配合&#xff0c;还是得自己思考&#xff0c;AI配合才能搞定&#xff0c;不思考只依赖AI也不行。内网服务器只是简单地使用了python -m http.server 8899&#xff0c;但是对于Gradio建立的服务器好像不行&#xff0c;会出…...

JWT认证实战

JWT&#xff08;JSON Web Token&#xff09;是一种轻量级的、基于 JSON 的开放标准&#xff08;RFC 7519&#xff09;&#xff0c;用于在各方之间安全地传递信息。JWT 的特点是结构简单、轻量化和跨平台支持&#xff0c;适用于用户身份验证、信息加密以及无状态的 API 访问控制…...

计算机网络 (23)IP层转发分组的过程

一、IP层的基本功能 IP层&#xff08;Internet Protocol Layer&#xff09;是网络通信模型中的关键层&#xff0c;属于OSI模型的第三层&#xff0c;即网络层。它负责在不同网络之间传输数据包&#xff0c;实现网络间的互联。IP层的主要功能包括寻址、路由、分段和重组、错误检测…...

权限管理的方法

模块化分类 功能模块划分 把人资管理系统按业务逻辑拆分成清晰的功能区&#xff0c;例如招聘管理、培训管理、绩效管理、员工档案管理等。招聘管理模块下还能细分职位发布、简历筛选、面试安排等子功能&#xff1b;员工档案管理涵盖基本信息、教育经历、工作履历录入与查询等。…...

【郑大主办、ACM出版、EI稳定检索】第四届密码学、网络安全与通信技术国际会议 (CNSCT 2025)

第四届密码学、网络安全与通信技术国际会议(CNSCT 2025)将于2025年1月17-19日在中国郑州盛大启幕&#xff08;线上召开&#xff09;。本次会议旨在汇聚全球密码学、网络安全与通信技术领域的顶尖学者、研究人员与行业领袖&#xff0c;共同探索计算机科学的最新进展与未来趋势。…...

48小时,搭建一个设备巡检报修系统

背景 时不时的&#xff0c;工地的设备又出了状况。巡检人员一顿懵逼、维修人员手忙脚乱&#xff0c;操作工人抱怨影响进度。老板看着待完成的订单&#xff0c;就差骂娘了&#xff1a;“这么搞下去&#xff0c;还能有效率吗&#xff1f;”。 于是&#xff0c;抱着试一试的心态…...

基于Redisson实现重入锁

一. 分布式锁基础 在分布式系统中&#xff0c;当多个客户端&#xff08;应用实例&#xff09;需要访问同一资源时&#xff0c;可以使用分布式锁来确保同一时刻只有一个客户端能访问该资源。Redis作为高性能的内存数据库&#xff0c;提供了基于键值对的分布式锁实现&#xff0c…...

Java文件操作的简单示例

使用原生库 创建空白文件 package com.company; import java.io.File; import java.io.IOException;public class Main {public static void main(String[] args) {File f new File("newfile.txt");try {boolean flag f.createNewFile();System.out.println(&quo…...

删除与增加特定行

1.删除特定行 new_df <- df[-c(4), ] #删除第4行 new_df <- df[-c(2:4), ] #去除第2-4行 new_df <- subset(df, col1 < 10 & col2 < 6) #删除特定第一列<10和第二列&#xff1c;6的行。按名字删除 无论行列&#xff0c;可以找出对应索引或构造相同长…...

动态规划六——两个数组的dp问题

目录 题目一——1143. 最长公共子序列 - 力扣&#xff08;LeetCode&#xff09; 题目二——1035. 不相交的线 - 力扣&#xff08;LeetCode&#xff09; 题目三——115. 不同的子序列 - 力扣&#xff08;LeetCode&#xff09; 题目四—— 44. 通配符匹配 - 力扣&#xff08;…...

项目优化之策略模式

目录 策略模式基本概念 策略模式的应用场景 实际项目中具体应用 项目背景&#xff1a; 策略模式解决方案&#xff1a; 计费模块策略模式简要代码 策略模式基本概念 策略模式(Strategy Pattern) 是一种行为型设计模式&#xff0c;把算法的使用放到环境类中&#xff0c;而算…...

[读书日志]从零开始学习Chisel 第四篇:Scala面向对象编程——操作符即方法(敏捷硬件开发语言Chisel与数字系统设计)

3.2操作符即方法 3.2.1操作符在Scala中的解释 在其它语言中&#xff0c;定义了一些基本的类型&#xff0c;但这些类型并不是我们在面向对象中所说的类。比如说1&#xff0c;这是一个int类型常量&#xff0c;但不能说它是int类型的对象。针对这些数据类型&#xff0c;存在一些…...

三子棋游戏

目录 1.创建项目 2.主函数编写 3.菜单函数编写 4.宏定义棋盘行和列 5.棋盘初始化 6.打印棋盘 7.玩家下棋 8.电脑下棋 9.平局判断 10.输赢判断 11.game函数 三子棋游戏&#xff08;通过改变宏定义可以变成五子棋&#xff09;&#xff0c;玩家与电脑下棋 1.创建项目…...

MyBatis执行一条sql语句的流程(源码解析)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 MyBatis执行一条sql语句的流程&#xff08;源码解析&#xff09; MyBatis执行sql语句的流程加载配置文件加载配置文件的流程 创建sqlsessionFactory对象解析Mapper创建sqlses…...

【电机控制】低通滤波器及系数配置

【电机控制】低通滤波器及系数配置 文章目录 [TOC](文章目录) 前言一、低通滤波器原理二、理论计算三、代码四、参考资料总结 前言 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、低通滤波器原理 二、理论计算 三、代码 //低通滤波 pv->Ealpha…...

ArcgisServer过了元旦忽然用不了了?许可过期

昨天过完元旦之后上班发现好多ArcgisServer的站点运行出错了&#xff0c;点击日志发现&#xff0c;说是许可过去&#xff0c;也就是当时安装ArcgisServer时读取的ecp文件过期了&#xff0c;需要重新读取。 解决方法 1.临时方法&#xff0c;修改系统时间&#xff0c;早于2024年…...

如何在不丢失数据的情况下从 IOS 14 回滚到 IOS 13

您是否后悔在 iPhone、iPad 或 iPod touch 上安装 iOS 14&#xff1f;如果你这样做&#xff0c;你并不孤单。许多升级到 iOS 14 beta 的 iPhone、iPad 和 iPod touch 用户不再适应它。 如果您在正式发布日期之前升级到 iOS 14 以享受其功能&#xff0c;但您不再适应 iOS 14&am…...

基于算法竞赛的c++编程(28)结构体的进阶应用

结构体的嵌套与复杂数据组织 在C中&#xff0c;结构体可以嵌套使用&#xff0c;形成更复杂的数据结构。例如&#xff0c;可以通过嵌套结构体描述多层级数据关系&#xff1a; struct Address {string city;string street;int zipCode; };struct Employee {string name;int id;…...

设计模式和设计原则回顾

设计模式和设计原则回顾 23种设计模式是设计原则的完美体现,设计原则设计原则是设计模式的理论基石, 设计模式 在经典的设计模式分类中(如《设计模式:可复用面向对象软件的基础》一书中),总共有23种设计模式,分为三大类: 一、创建型模式(5种) 1. 单例模式(Sing…...

基于Flask实现的医疗保险欺诈识别监测模型

基于Flask实现的医疗保险欺诈识别监测模型 项目截图 项目简介 社会医疗保险是国家通过立法形式强制实施&#xff0c;由雇主和个人按一定比例缴纳保险费&#xff0c;建立社会医疗保险基金&#xff0c;支付雇员医疗费用的一种医疗保险制度&#xff0c; 它是促进社会文明和进步的…...

oracle与MySQL数据库之间数据同步的技术要点

Oracle与MySQL数据库之间的数据同步是一个涉及多个技术要点的复杂任务。由于Oracle和MySQL的架构差异&#xff0c;它们的数据同步要求既要保持数据的准确性和一致性&#xff0c;又要处理好性能问题。以下是一些主要的技术要点&#xff1a; 数据结构差异 数据类型差异&#xff…...

python爬虫:Newspaper3k 的详细使用(好用的新闻网站文章抓取和解析的Python库)

更多内容请见: 爬虫和逆向教程-专栏介绍和目录 文章目录 一、Newspaper3k 概述1.1 Newspaper3k 介绍1.2 主要功能1.3 典型应用场景1.4 安装二、基本用法2.2 提取单篇文章的内容2.2 处理多篇文档三、高级选项3.1 自定义配置3.2 分析文章情感四、实战案例4.1 构建新闻摘要聚合器…...

mysql已经安装,但是通过rpm -q 没有找mysql相关的已安装包

文章目录 现象&#xff1a;mysql已经安装&#xff0c;但是通过rpm -q 没有找mysql相关的已安装包遇到 rpm 命令找不到已经安装的 MySQL 包时&#xff0c;可能是因为以下几个原因&#xff1a;1.MySQL 不是通过 RPM 包安装的2.RPM 数据库损坏3.使用了不同的包名或路径4.使用其他包…...

sipsak:SIP瑞士军刀!全参数详细教程!Kali Linux教程!

简介 sipsak 是一个面向会话初始协议 (SIP) 应用程序开发人员和管理员的小型命令行工具。它可以用于对 SIP 应用程序和设备进行一些简单的测试。 sipsak 是一款 SIP 压力和诊断实用程序。它通过 sip-uri 向服务器发送 SIP 请求&#xff0c;并检查收到的响应。它以以下模式之一…...

C++使用 new 来创建动态数组

问题&#xff1a; 不能使用变量定义数组大小 原因&#xff1a; 这是因为数组在内存中是连续存储的&#xff0c;编译器需要在编译阶段就确定数组的大小&#xff0c;以便正确地分配内存空间。如果允许使用变量来定义数组的大小&#xff0c;那么编译器就无法在编译时确定数组的大…...

LINUX 69 FTP 客服管理系统 man 5 /etc/vsftpd/vsftpd.conf

FTP 客服管理系统 实现kefu123登录&#xff0c;不允许匿名访问&#xff0c;kefu只能访问/data/kefu目录&#xff0c;不能查看其他目录 创建账号密码 useradd kefu echo 123|passwd -stdin kefu [rootcode caozx26420]# echo 123|passwd --stdin kefu 更改用户 kefu 的密码…...

MySQL 索引底层结构揭秘:B-Tree 与 B+Tree 的区别与应用

文章目录 一、背景知识&#xff1a;什么是 B-Tree 和 BTree&#xff1f; B-Tree&#xff08;平衡多路查找树&#xff09; BTree&#xff08;B-Tree 的变种&#xff09; 二、结构对比&#xff1a;一张图看懂 三、为什么 MySQL InnoDB 选择 BTree&#xff1f; 1. 范围查询更快 2…...