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. 给线程加锁(互斥锁)
原理:
通过对热点数据的访问加互斥锁(如分布式锁或本地锁),保证在缓存失效后,只有一个线程能去查询数据库并更新缓存,其余线程等待锁释放后从缓存读取数据。
优点:
- 简单易实现:逻辑清晰,只需引入锁机制即可。
- 保护数据库:有效防止多线程同时查询数据库,降低数据库压力。
- 通用性强:适用于所有数据场景,尤其是并发量高的热点数据。
缺点:
- 性能问题:
- 在高并发情况下,锁的排队等待会增加响应时间。
- 如果锁竞争激烈,会导致线程阻塞。
- 单点问题:
- 如果锁是本地实现,可能会出现分布式环境中的一致性问题。
- 使用分布式锁(如 Redis 分布式锁)会增加实现复杂度。
- 潜在死锁风险:如果锁机制设计不当,可能会导致死锁。
使用互斥锁的代码逻辑:
查询 Redis:
- 首先查询 Redis 是否存在目标数据。
- 如果命中,直接返回。
- 如果未命中(Redis 中没有目标数据),进入下一步。
-
尝试获取锁:
如果获取锁成功:
- 再次查询 Redis 是否有数据(防止其他线程已加载数据)。
- 如果 Redis 中仍然没有数据,查询数据库,加载数据到 Redis。
- 释放锁,返回数据。
如果未获取到锁:
- 等待一段时间后,重复尝试获取锁。
- 在每次尝试获取锁时,先查询 Redis 是否已存在数据,防止无意义的锁竞争。
为什么获取锁之后还需要再次检查Redis中是否存在数据?
核心原因: 避免重复查询数据库和重复写入 Redis,从而减少资源浪费。
多线程场景下的问题:
- 假设线程 A 获取到锁并完成了缓存重建(从数据库查询数据并写入 Redis)。
- 线程 B 在等待锁的过程中,其实线程 A 已经完成了数据的缓存。
- 线程 B 获取到锁时,如果不再次检查 Redis,就会重复从数据库查询并覆盖 Redis 中的已有数据,导致不必要的开销和延迟。
这个锁应该是什么样的锁?是平常的吗
使用 Redis 实现分布式锁
Redis 是实现分布式锁的常用工具。通过 SETNX
(set 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. 逻辑过期
原理:
将缓存数据设置为逻辑过期时间,同时保留旧值。当缓存过期时,请求仍然返回旧数据,同时后台异步更新缓存(通过任务队列或线程池刷新数据)。
优点:
- 高性能:
- 由于返回旧数据,请求不会直接打到数据库,避免了数据库的压力。
- 不会阻塞用户线程,用户体验更好。
- 无锁机制:
- 通过异步任务更新缓存,避免了加锁的复杂性和性能问题。
- 支持高并发:
- 用户请求不会因缓存失效而阻塞。
缺点:
- 数据一致性问题:
- 在缓存逻辑过期的时间段内,可能返回的是旧数据,适合对一致性要求不高的场景。
- 实现复杂度高:
- 需要设计异步更新逻辑,增加系统复杂性。
- 需要引入额外的定时任务或异步线程池处理刷新逻辑。
- 资源占用:
- 异步更新的任务可能会带来额外的资源消耗,尤其是在数据量较大时。
选择建议
- 互斥锁适合对数据一致性要求高的业务场景,如订单、库存等核心数据,且并发量较低。
- 逻辑过期更适合高并发、热点数据且对数据一致性要求不高的场景,如新闻热点、排行榜等。
逻辑过期方式解决问题的逻辑:
查询缓存数据
- 通过
Redis
的key
查询数据,结果可能是:- 缓存未命中:直接返回
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;
为什么需要进行二次校验?
以下是可能的线程执行时间线:
- 线程A:发现缓存过期,获取锁,进入重建逻辑。
- 线程B:也发现缓存过期,但未获取到锁,进入等待状态。
- 线程A:完成缓存重建,更新Redis数据,释放锁。
- 线程B:获取到锁,继续执行(认为缓存仍然过期)。
- 如果没有二次校验,线程B会再次重建缓存。
- 如果有二次校验,线程B会发现缓存数据已经更新,不需要重复重建。
此时,如果线程A完成缓存重建并释放锁后,线程B再次获取锁,直接重建缓存,会造成:
- 重复的缓存重建(浪费资源)。
- 数据被多次写入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
:适配不同的对象类型(如Shop
、User
等)。 - 定义主键类型
ID
:适配不同的数据主键(如Long
或String
)。
- 定义返回值类型
public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit
) {// 逻辑与实现
}
R
代表返回的实体类类型,如Shop
、User
等。ID
代表主键的类型,如Long
(数字型 ID)或String
(UUID)。
2. 如何根据调用者需求获取数据库数据
关键点:调用者可能有不同的需求,例如:
- 查询逻辑不同(按 ID、按名称等)。
- 数据返回类型不同(单个对象、列表等)。
- 数据库表不同。
解决方案: 使用 Function<ID, R>
获取数据
Function<ID, R>
的优势
- 灵活性:调用者可以动态传递 Lambda 表达式,实现灵活的查询逻辑。
- 解耦:工具类不需要依赖具体的服务或 DAO 实现,而是将查询逻辑交给调用者。
- 简单易用:调用者可以用 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查询缓存
什么是缓存? 缓存是一种提高数据访问效率的技术,通过在内存中存储数据的副本来减少对数据库或其他慢速存储设备的频繁访问。缓存通常用于存储热点数据或计算代价高的结果,以加快响应速度。 添加Redis缓存有什么好处? Redis 基…...

双馈风电DFIG并网系统次转子侧变流器RSC抑制策略研究基于LADRC和重复控制的方法
风电装机容量的持续增长以及电力电子装置的大规模接入,导致电网强度降低,系 统运行特性发生深刻变化,严重威胁风电并网系统的安全稳定运行。因此本文以双馈风 电场经串补线路并网系统为研究对象,在深入分析双馈风电并网系统振荡…...

国产编辑器EverEdit - 使用技巧:变量重命名的一种简单替代方法
1 使用技巧:变量重命名的一种简单替代方法 1.1 应用场景 写过代码的都知道,经常添加功能的时候,是把别的地方的代码拷贝过来,改吧改吧,就能用了,改的过程中,就涉及到一个变量名的问题ÿ…...

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

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

计算机网络 (23)IP层转发分组的过程
一、IP层的基本功能 IP层(Internet Protocol Layer)是网络通信模型中的关键层,属于OSI模型的第三层,即网络层。它负责在不同网络之间传输数据包,实现网络间的互联。IP层的主要功能包括寻址、路由、分段和重组、错误检测…...
权限管理的方法
模块化分类 功能模块划分 把人资管理系统按业务逻辑拆分成清晰的功能区,例如招聘管理、培训管理、绩效管理、员工档案管理等。招聘管理模块下还能细分职位发布、简历筛选、面试安排等子功能;员工档案管理涵盖基本信息、教育经历、工作履历录入与查询等。…...

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

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

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

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和第二列<6的行。按名字删除 无论行列,可以找出对应索引或构造相同长…...

动态规划六——两个数组的dp问题
目录 题目一——1143. 最长公共子序列 - 力扣(LeetCode) 题目二——1035. 不相交的线 - 力扣(LeetCode) 题目三——115. 不同的子序列 - 力扣(LeetCode) 题目四—— 44. 通配符匹配 - 力扣(…...
项目优化之策略模式
目录 策略模式基本概念 策略模式的应用场景 实际项目中具体应用 项目背景: 策略模式解决方案: 计费模块策略模式简要代码 策略模式基本概念 策略模式(Strategy Pattern) 是一种行为型设计模式,把算法的使用放到环境类中,而算…...

[读书日志]从零开始学习Chisel 第四篇:Scala面向对象编程——操作符即方法(敏捷硬件开发语言Chisel与数字系统设计)
3.2操作符即方法 3.2.1操作符在Scala中的解释 在其它语言中,定义了一些基本的类型,但这些类型并不是我们在面向对象中所说的类。比如说1,这是一个int类型常量,但不能说它是int类型的对象。针对这些数据类型,存在一些…...
三子棋游戏
目录 1.创建项目 2.主函数编写 3.菜单函数编写 4.宏定义棋盘行和列 5.棋盘初始化 6.打印棋盘 7.玩家下棋 8.电脑下棋 9.平局判断 10.输赢判断 11.game函数 三子棋游戏(通过改变宏定义可以变成五子棋),玩家与电脑下棋 1.创建项目…...

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

【电机控制】低通滤波器及系数配置
【电机控制】低通滤波器及系数配置 文章目录 [TOC](文章目录) 前言一、低通滤波器原理二、理论计算三、代码四、参考资料总结 前言 提示:以下是本篇文章正文内容,下面案例可供参考 一、低通滤波器原理 二、理论计算 三、代码 //低通滤波 pv->Ealpha…...
ArcgisServer过了元旦忽然用不了了?许可过期
昨天过完元旦之后上班发现好多ArcgisServer的站点运行出错了,点击日志发现,说是许可过去,也就是当时安装ArcgisServer时读取的ecp文件过期了,需要重新读取。 解决方法 1.临时方法,修改系统时间,早于2024年…...

如何在不丢失数据的情况下从 IOS 14 回滚到 IOS 13
您是否后悔在 iPhone、iPad 或 iPod touch 上安装 iOS 14?如果你这样做,你并不孤单。许多升级到 iOS 14 beta 的 iPhone、iPad 和 iPod touch 用户不再适应它。 如果您在正式发布日期之前升级到 iOS 14 以享受其功能,但您不再适应 iOS 14&am…...

【快手拥抱开源】通过快手团队开源的 KwaiCoder-AutoThink-preview 解锁大语言模型的潜力
引言: 在人工智能快速发展的浪潮中,快手Kwaipilot团队推出的 KwaiCoder-AutoThink-preview 具有里程碑意义——这是首个公开的AutoThink大语言模型(LLM)。该模型代表着该领域的重大突破,通过独特方式融合思考与非思考…...

什么是库存周转?如何用进销存系统提高库存周转率?
你可能听说过这样一句话: “利润不是赚出来的,是管出来的。” 尤其是在制造业、批发零售、电商这类“货堆成山”的行业,很多企业看着销售不错,账上却没钱、利润也不见了,一翻库存才发现: 一堆卖不动的旧货…...
python如何将word的doc另存为docx
将 DOCX 文件另存为 DOCX 格式(Python 实现) 在 Python 中,你可以使用 python-docx 库来操作 Word 文档。不过需要注意的是,.doc 是旧的 Word 格式,而 .docx 是新的基于 XML 的格式。python-docx 只能处理 .docx 格式…...
解决本地部署 SmolVLM2 大语言模型运行 flash-attn 报错
出现的问题 安装 flash-attn 会一直卡在 build 那一步或者运行报错 解决办法 是因为你安装的 flash-attn 版本没有对应上,所以报错,到 https://github.com/Dao-AILab/flash-attention/releases 下载对应版本,cu、torch、cp 的版本一定要对…...
CMake控制VS2022项目文件分组
我们可以通过 CMake 控制源文件的组织结构,使它们在 VS 解决方案资源管理器中以“组”(Filter)的形式进行分类展示。 🎯 目标 通过 CMake 脚本将 .cpp、.h 等源文件分组显示在 Visual Studio 2022 的解决方案资源管理器中。 ✅ 支持的方法汇总(共4种) 方法描述是否推荐…...

分布式增量爬虫实现方案
之前我们在讨论的是分布式爬虫如何实现增量爬取。增量爬虫的目标是只爬取新产生或发生变化的页面,避免重复抓取,以节省资源和时间。 在分布式环境下,增量爬虫的实现需要考虑多个爬虫节点之间的协调和去重。 另一种思路:将增量判…...

VM虚拟机网络配置(ubuntu24桥接模式):配置静态IP
编辑-虚拟网络编辑器-更改设置 选择桥接模式,然后找到相应的网卡(可以查看自己本机的网络连接) windows连接的网络点击查看属性 编辑虚拟机设置更改网络配置,选择刚才配置的桥接模式 静态ip设置: 我用的ubuntu24桌…...

R语言速释制剂QBD解决方案之三
本文是《Quality by Design for ANDAs: An Example for Immediate-Release Dosage Forms》第一个处方的R语言解决方案。 第一个处方研究评估原料药粒径分布、MCC/Lactose比例、崩解剂用量对制剂CQAs的影响。 第二处方研究用于理解颗粒外加硬脂酸镁和滑石粉对片剂质量和可生产…...
【Nginx】使用 Nginx+Lua 实现基于 IP 的访问频率限制
使用 NginxLua 实现基于 IP 的访问频率限制 在高并发场景下,限制某个 IP 的访问频率是非常重要的,可以有效防止恶意攻击或错误配置导致的服务宕机。以下是一个详细的实现方案,使用 Nginx 和 Lua 脚本结合 Redis 来实现基于 IP 的访问频率限制…...

mac 安装homebrew (nvm 及git)
mac 安装nvm 及git 万恶之源 mac 安装这些东西离不开Xcode。及homebrew 一、先说安装git步骤 通用: 方法一:使用 Homebrew 安装 Git(推荐) 步骤如下:打开终端(Terminal.app) 1.安装 Homebrew…...