Day77(3)-F:\code\hm-dianping\hm-dianping\src\main\java\com\hmdp\service\impl\ShopServiceImpl.java-redis基础
黑马点评
商户查询缓存
添加Redis缓存
根据id查询商铺信息-controller
/**
* 根据id查询商铺信息
* @param id 商铺id
* @return 商铺详情数据
*/
@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {
return shopService.queryById(id);
}
根据id查询商铺信息-serviceimpl
@Override
public Result queryById(Long id) {
//1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
//2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
//3.存在,直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
//4.不存在,根据id查询数据库
Shop shop = getById(id);
//5.不存在,返回错误
if (shop == null){
return Result.fail("店铺不存在!");
}
//6.存在,写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(shop));
//7.返回
return Result.ok(shop);
}
查询商铺种类信息-controller
package com.hmdp.controller;
import com.hmdp.dto.Result;
import com.hmdp.entity.ShopType;
import com.hmdp.service.IShopTypeService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
/**
* <p>
* 前端控制器
* </p>
*
* @author 虎哥
* @since 2021-12-22
*/
@RestController
@RequestMapping("/shop-type")
public class ShopTypeController {
@Resource
private IShopTypeService typeService;
@GetMapping("list")
public Result queryTypeList() {
// List<ShopType> typeList = typeService
// .query().orderByAsc("sort").list();
// return Result.ok(typeList);
return typeService.queryAll();
}
}
查询商铺种类信息-serviceimpl
package com.hmdp.service.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.conditions.query.QueryChainWrapper;
import com.hmdp.dto.Result;
import com.hmdp.entity.ShopType;
import com.hmdp.mapper.ShopTypeMapper;
import com.hmdp.service.IShopTypeService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import static com.hmdp.utils.RedisConstants.CACHE_SHOP_TYPE_KEY;
/**
* <p>
* 服务实现类
* </p>
*
* @author 虎哥
* @since 2021-12-22
*/
@Service
public class ShopTypeServiceImpl extends ServiceImpl<ShopTypeMapper, ShopType> implements IShopTypeService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryAll() {
//1.先在redis查询
String shopType = stringRedisTemplate.opsForValue().get(CACHE_SHOP_TYPE_KEY);
//2.如果有,直接返回
if (StrUtil.isNotBlank(shopType)) {
List<ShopType> shopTypeList = JSONUtil.toList(shopType, ShopType.class);
return Result.ok(shopTypeList);
}
//3.如果没有,再去数据库找
List<ShopType> shopTypeList = query().orderByAsc("sort").list();
//4.如果数据库没有,返回错误
if (shopTypeList == null||shopTypeList.isEmpty()){
return Result.fail("店铺种类数据丢失");
}
//5.如果数据库有,添加到redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_TYPE_KEY,JSONUtil.toJsonStr(shopTypeList));
//6.返回
return Result.ok(shopTypeList);
}
}
缓存更新策略
技术栈
- 写数据:先写数据库再删缓存;读数据:读就先看缓存有没有,如果有就返回,没有再去查数据库,并写入缓存,设置超时时间
- 更新数据,先改数据库,再删除缓存
1.写数据:先写数据库再删缓存;读数据:读就先看缓存有没有,如果有就返回,没有再去查数据库,并写入缓存,设置超时时间
读数据设置超时时间(50)
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 org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
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
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryById(Long id) {
//1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
//2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
//3.存在,直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
//4.不存在,根据id查询数据库
Shop shop = getById(id);
//5.不存在,返回错误
if (shop == null){
return Result.fail("店铺不存在!");
}
//6.存在,写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
//7.返回
return Result.ok(shop);
}
}
更新数据要先改数据库再删除缓存
/**
* 更新商铺信息
* @param shop 商铺数据
* @return 无
*/
@PutMapping
public Result updateShop(@RequestBody Shop shop) {
// 写入数据库
return shopService.update(shop);
}
2.更新数据,先改数据库,再删除缓存
@Override
@Transactional
public Result update(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();
}
缓存穿透
知识点
布隆过滤原理
- 底层核心组成
布隆过滤器的底层主要由两部分组成:
• 位数组(Bit Array):一个长度为 的位数组,初始状态下所有位都设置为 0。
• 哈希函数(Hash Functions): 个独立的、且能够将输入均匀分布的无偏哈希函数。
 - 工作原理:添加与查询
A. 元素写入(Add)
当我们要向布隆过滤器中添加一个元素(例如:用户 ID "123")时: - 分别使用 个哈希函数对该元素进行计算,得到 个哈希值。
- 将这 个哈希值对位数组长度 取模,得到 个在 范围内的索引位置。
- 将位数组中对应的这 个位置的值全部置为 1。
B. 元素查询(Query)
当我们要查询一个元素是否存在时: - 同样使用那 个哈希函数计算出 个索引位置。
- 检查位数组中这 个位置的值:
• 只要有一个位置为 0:该元素一定不存在于集合中。
• 所有位置都为 1:该元素可能存在(也可能是因为其他多个元素刚好把这些位置都占满了,产生了“哈希碰撞”)。
 - 为什么会产生误判?
误判(False Positive)是布隆过滤器的天性。 假设元素 A 占用了位置 1, 3, 5,元素 B 占用了位置 2, 4, 6。此时我们要查询元素 C,如果 C 的哈希计算结果刚好是 1, 4, 6,你会发现这三个位置都是 1,于是布隆过滤器会告诉你“C 存在”,但实际上 C 从未被添加过。
影响误判率的因素:
• 位数组长度 :数组越大,位被占满的速度越慢,误判率越低。
• 哈希函数个数 :函数太少容易碰撞;函数太多会导致位数组迅速变满。
• 存储元素个数 :存入的元素越多,误判率越高。
技术栈
- 通过数据库向redis存入一个空值解决缓存穿透问题),设置TTL为两分钟(21-23),如果是空值缓存在redis直接在redis层返回错误信息(12-16)
1.通过数据库向redis存入一个空值解决缓存穿透问题,设置TTL为两分钟(21-23),如果是空值缓存在redis直接在redis层返回错误信息(12-16)
@Override
public Result queryById(Long id) {
String key = CACHE_SHOP_KEY + id;
//1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
//3.存在,直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
//判断命中的是否为空值
if (shopJson!=null){
// 返回一个错误信息
return Result.fail("店铺信息不存在!");
}
//4.不存在,根据id查询数据库
Shop shop = getById(id);
//5.不存在,返回错误
if (shop == null){
//将空值写入redis
//返回错误信息
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
return Result.fail("店铺不存在!");
}
//6.存在,写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
//7.返回
return Result.ok(shop);
}
知识点
通过雪花id和后端给前端返回时再加密保护数据库不被攻击
HashID:这是目前很流行的做法。数据库内部依然用自增 ID 或雪花 ID,但在返回给前端接口时,通过算法将其加密成一段看起来像乱码的字符串(如 jRwl8Y9)。
缓存雪崩
利用集群(高级板块)
降级限流(springcloud)
缓存击穿
知识点
目前主流的方案主要分为两派:基于 Redis 的 Redisson 方案 和 基于 ZooKeeper 的临时顺序节点方案。
1. 工业级标准:Redisson 自研/集成方案
在 Java 生态中,几乎 90% 的企业在使用 Redis 时都会直接选择 Redisson,而不是自己写原生的 setnx。
A. 解决“锁续期”问题(看门狗机制)
在实际业务中,我们很难预估一个任务执行到底需要多久。
- 痛点:过期时间设长了,宕机后锁释放慢;设短了,任务没跑完锁就丢了。
- 设计:Redisson 引入了 Watch Dog(看门狗)。只要业务没执行完,看门狗会每隔 10 秒(默认)帮你把锁的过期时间重新重置为 30 秒。这样既保证了任务执行期间锁不丢,又保证了宕机后锁能自动释放。
B. 解决“可重入”问题
企业设计要求同一个线程在持有锁的情况下,可以多次进入同一把锁的代码块。
- 设计:底层不再使用简单的
String存储,而是使用 Redis Hash。- Key:锁的名字。
- Field:线程标识(UUID + ThreadID)。
- Value:锁的计数器(Counter)。每次进入 +1,退出 -1,减到 0 时删除 Key。
2. 追求强一致性:ZooKeeper 方案
如果业务场景对“绝对不能出差错”有极高要求(如金融转账),企业往往会选择 ZooKeeper (ZK)。
设计逻辑:
- 临时顺序节点:每个请求进来,在 ZK 的锁路径下创建一个带序号的临时节点。
- 最小序号获胜:谁的序号最小,谁就获得锁。
- 监听机制(Watch):没拿到锁的线程不进行死循环重试,而是监听前一个节点的删除事件。前一个节点一旦消失(业务完成或崩溃),ZK 会立即通知下一个节点。
3. 企业设计中的“大坑”:主从切换下的锁丢失
这是面试常问,也是架构设计必须考虑的问题。
- 场景:你在 Redis 主节点加了锁,主节点还没来得及把数据同步给从节点就挂了。从节点升级为主节点,此时你的锁丢了,另一个线程进来又能加锁成功。
- 企业对策:
- Redlock(红锁)算法:向 5 个独立的 Redis 实例请求加锁,只有超过半数(3个)成功才算成功。虽然安全,但性能损耗大,目前争议较大。
- 业务补偿/幂等设计:大部分企业接受极小概率的锁丢失,但在数据库层面通过 CAS(乐观锁) 或 唯一索引 做最终兜底,确保即便锁失效,数据也不会乱。
技术栈
- 互斥锁解决
- 逻辑过期解决
- 调用线程池中的线程进行线程重建(不要去新建一个线程)
1.互斥锁解决queryWithMutex(Long id)(102-160):a.递归互斥锁看看能不能获取到(124-128),递归过程中,可能其他线程完成了缓存更新,可以直接返回;b.如果上一个锁持有者修改成功直接把锁释放,该线程又恰好在执行完查询那一刻拿到锁,则有可能跳转到数据库,为了避免这种情况,再执行一次redis查询(130-137)
package com.hmdp.service.impl;
import cn.hutool.core.util.BooleanUtil;
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 org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
import static com.hmdp.utils.RedisConstants.*;
/**
* <p>
* 服务实现类
* </p>
*
* @author 虎哥
* @since 2021-12-22
*/
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryById(Long id) {
//缓存穿透
//Shop shop = queryWithPassThrough(id);
//通过互斥锁解决缓存击穿
Shop shop = queryWithMutex(id);
if (shop == null) {
return Result.fail("店铺不存在!");
}
//7.返回
return Result.ok(shop);
}
/**
* 上锁
* @param key
* @return
*/
private boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
/**
* 释放锁
* @param key
*/
private void unLock(String key){
stringRedisTemplate.delete(key);
}
/**
* 解决缓存穿透
* @param id
* @return
*/
private Shop queryWithPassThrough(Long id){
String key = CACHE_SHOP_KEY + id;
//1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
//3.存在,直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return shop;
}
//判断命中的是否为空值
if (shopJson!=null){
// 返回一个错误信息
return null;
}
//4.不存在,根据id查询数据库
Shop shop = getById(id);
//5.不存在,返回错误
if (shop == null){
//将空值写入redis
//返回错误信息
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
return null;
}
//6.存在,写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
//7.返回
return shop;
}
/**
* 互斥锁,解决缓存击穿
* @param id
* @return
*/
private Shop queryWithMutex(Long id){
String key = CACHE_SHOP_KEY + id;
//1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
//3.存在,直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return shop;
}
//4.实现缓存重建
//4.1获取互斥锁
String lockKey = LOCK_SHOP_KEY+id;
Shop shop = null;
try {
boolean isLock = tryLock(lockKey);
//4.2判断是否获取成功
if (!isLock){
//4.3失败,休眠并重试
Thread.sleep(50);
//递归
return queryWithMutex(id);
}
//1.从redis查询商铺缓存
String shop_Json = stringRedisTemplate.opsForValue().get(key);
//2.判断是否存在
if (StrUtil.isNotBlank(shop_Json)) {
//3.存在,直接返回
shop = JSONUtil.toBean(shop_Json, Shop.class);
return shop;
}
//4.4成功,根据id查询数据库
shop = getById(id);
//本地查询快不容易报错,先休眠模拟并发,模拟重建的延时
Thread.sleep(200);
//5.不存在,返回错误
if (shop == null){
//将空值写入redis
//返回错误信息
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
return null;
}
//6.存在,写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
//7.释放互斥锁
unLock(lockKey);
}
//8.返回
return shop;
}
@Override
@Transactional
public Result update(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不存在会直接返回空,都需要二次检查(30-45)
不需要修改原本的实体
@Data
public class RedisData {
private LocalDateTime expireTime;
private Object data;
}
3.调用线程池中的线程进行线程重建(不要去新建一个线程)(1,47-52)
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
private Shop queryWithLogicalExpire(Long id){
String key = CACHE_SHOP_KEY + id;
//1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2.判断是否不存在
if (StrUtil.isBlank(shopJson)) {
//3.不存在,直接返回
return null;
}
//4.命中,需要先把json反序列化为对象
// RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
// JSONObject data = (JSONObject)redisData.getData();
// Shop shop = JSONUtil.toBean(data, Shop.class);//也可以用泛型,就可以直接得到Shop类
RedisData<Shop> redisData = JSONUtil.toBean(shopJson, new TypeReference<RedisData<Shop>>() {}, false);
Shop shop = redisData.getData();
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){
//需要再次检查redis缓存是否过期
//1.从redis查询商铺缓存
shopJson = stringRedisTemplate.opsForValue().get(key);
//2.判断是否不存在
if (StrUtil.isBlank(shopJson)) {
//3.不存在,直接返回
return null;
}
redisData = JSONUtil.toBean(shopJson, new TypeReference<RedisData<Shop>>() {}, false);
shop = redisData.getData();
expireTime = redisData.getExpireTime();
//5.判断是否过期
if (expireTime.isAfter(LocalDateTime.now())){
//5.1未过期,直接返回信息
return shop;
}
//还是已过期且成功获取到锁
//6.3成功,开启独立线程,实现缓存重建,通过线程池
CACHE_REBUILD_EXECUTOR.submit(()->{
try {
//执行缓存重建
saveShop2Redis(id,20L);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//一定要释放锁
unLock(lockKey);
}
});
}
//6.4无论成功失败都获取缓存信息
//7.返回
return shop;
}
知识点
由于 RedisData 里的 data 属性是 Object 类型的,JSON 工具在反序列化时,并不知道这个 Object 具体是 Shop 还是 User。为了保险,它会先把它转成一个中间形态:JSONObject(本质上是一个 Map)。
缓存工具类封装
技术栈
- 泛型函数抽取
- 拆包类型转换
1.泛型函数抽取(49、80)
2.拆包类型转换(94-95,118-119)
package com.hmdp.utils;
import cn.hutool.core.lang.TypeReference;
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.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.Year;
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
public class CacheClient {
private final StringRedisTemplate stringRedisTemplate;
public CacheClient(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public void set(String key, Object value, Long time, TimeUnit unit){
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
}
public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit){
//设置逻辑过期
RedisData<Object> redisData = RedisData.builder()
.data(value).expireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time))).build();
//写入redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}
/**
* 解决缓存穿透
* @param id
* @return
*/
public <R,ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback, Long time, TimeUnit unit){
String key = keyPrefix + id;
//1.从redis查询商铺缓存
String json = stringRedisTemplate.opsForValue().get(key);
//2.判断是否存在
if (StrUtil.isNotBlank(json)) {
//3.存在,直接返回
return JSONUtil.toBean(json, type);
}
//判断命中的是否为空值
if (json!=null){
// 返回一个错误信息
return null;
}
//4.不存在,根据id查询数据库
R r = dbFallback.apply(id);
//5.不存在,返回错误
if (r == null){
//将空值写入redis
//返回错误信息
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
return null;
}
//6.存在,写入redis
this.set(key,r,time,unit);
//7.返回
return r;
}
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public <R,ID> R queryWithLogicalExpire(String keyPrefix,ID id,Class<R> type,Function<ID,R> dbFallBack, Long time, TimeUnit unit){
String key = keyPrefix + id;
//1.从redis查询商铺缓存
String json = stringRedisTemplate.opsForValue().get(key);
//2.判断是否不存在
if (StrUtil.isBlank(json)) {
//3.不存在,直接返回
return null;
}
//4.命中,需要先把json反序列化为对象
// RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
// JSONObject data = (JSONObject)redisData.getData();
// Shop shop = JSONUtil.toBean(data, Shop.class);//也可以用泛型,就可以直接得到Shop类
RedisData<R> redisData = JSONUtil.toBean(json, new TypeReference<RedisData<R>>() {}, false);
Object object = redisData.getData();
R r = JSONUtil.toBean((JSONObject) object, type);
LocalDateTime expireTime = redisData.getExpireTime();
//5.判断是否过期
if (expireTime.isAfter(LocalDateTime.now())){
//5.1未过期,直接返回信息
return r;
}
//5.2已过期,需要缓存重建
//6.缓存重建
//6.1获取互斥锁
String lockKey = LOCK_SHOP_KEY+id;
boolean isLock = tryLock(lockKey);
//6.2判断是否获取锁成功
if (isLock){
//需要再次检查redis缓存是否过期
//1.从redis查询商铺缓存
json = stringRedisTemplate.opsForValue().get(key);
//2.判断是否不存在
if (StrUtil.isBlank(json)) {
//3.不存在,直接返回
return null;
}
redisData = JSONUtil.toBean(json, new TypeReference<RedisData<R>>() {}, false);
object = redisData.getData();
r = JSONUtil.toBean((JSONObject) object, type);
expireTime = redisData.getExpireTime();
//5.判断是否过期
if (expireTime.isAfter(LocalDateTime.now())){
//5.1未过期,直接返回信息
unLock(lockKey);
return r;
}
//还是已过期且成功获取到锁
//6.3成功,开启独立线程,实现缓存重建,通过线程池
CACHE_REBUILD_EXECUTOR.submit(()->{
try {
//先查数据库
R r1 = dbFallBack.apply(id);
//写入redis
this.setWithLogicalExpire(key,r1,time,unit);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//一定要释放锁
unLock(lockKey);
}
});
}
//6.4无论成功失败都获取缓存信息
//7.返回
return r;
}
/**
* 上锁
* @param key
* @return
*/
private boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
/**
* 释放锁
* @param key
*/
private void unLock(String key){
stringRedisTemplate.delete(key);
}
}
补充技术
- 自定义线程池,不用系统默认的无界线程池,提高性能
- 锁标识传递 ,主线程把自己的线程id传给重建缓存的线程执行
- 企业级CANAL,只查询缓存,将增删改操作对应的更新缓存通过mq执行
1.自定义线程池,不用系统默认的无界线程池,提高性能
在线程池中,通常有“发布任务的人”(主线程/请求线程)和“干活的人”(线程池里的线程)。当线程池已经满负荷(线程全忙且队列全满)时,正常的流程是拒绝新任务。而 CallerRunsPolicy 会让主线程不再去接收新活,而是亲自下场把自己刚才提交的那个任务给执行了。
1. 行为转变:从“异步”变成“同步”
- 正常情况:主线程执行
submit,任务进队列,主线程立即执行return r(异步刷新缓存)。 - 策略触发时:主线程执行
submit发现塞不进去了,于是主线程停下来,亲自去跑dbFallBack.apply(id)查数据库、写缓存。跑完之后,主线程才会继续往下走执行return r。
2. 这个策略对你系统的 3 大好处
- 保证锁一定能释放: 你最担心的就是锁释放问题。在
AbortPolicy(抛异常)下,主线程可能崩了导致没走unLock。但在CallerRunsPolicy下,任务是在主线程跑的,执行完后必然会触发finally块里的unLock(lockKey, currentLockValue)。 - 自动降速(负压反馈): 当主线程亲自去查数据库(假设耗时 500ms)时,它在这 500ms 内无法接收新的 HTTP 请求。这就像是系统自动告诉前端:“我太忙了,慢点发请求过来”,从而保护了后端不会被瞬间的高并发冲垮。
- 重建不丢失: 任务不会被直接丢弃。即使重建变慢了,它最终还是会被执行,确保了缓存迟迟得不到更新的时间被缩短。
//private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
// 使用 ThreadPoolExecutor 手动创建,不再使用 Executors 快捷工厂
private static final ExecutorService CACHE_REBUILD_EXECUTOR = new ThreadPoolExecutor(
10, // 核心线程数
20, // 最大线程数
1L, TimeUnit.MINUTES, // 空闲线程存活时间
new ArrayBlockingQueue<>(100), // 有界队列:最多排队 100 个,超过就触发拒绝策略
new ThreadPoolExecutor.CallerRunsPolicy() // 关键:拒绝策略改为“调用者运行”
);
2.锁标识传递 ,主线程把自己的线程id传给重建缓存的线程执行(27),如果tryLock单参数的效果好一点
public <R,ID> R queryWithLogicalExpire(String keyPrefix,ID id,Class<R> type,Function<ID,R> dbFallBack, Long time, TimeUnit unit){
String key = keyPrefix + id;
//1.从redis查询商铺缓存
String json = stringRedisTemplate.opsForValue().get(key);
//2.判断是否不存在
if (StrUtil.isBlank(json)) {
//3.不存在,直接返回
return null;
}
//4.命中,需要先把json反序列化为对象
// RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
// JSONObject data = (JSONObject)redisData.getData();
// Shop shop = JSONUtil.toBean(data, Shop.class);//也可以用泛型,就可以直接得到Shop类
RedisData<R> redisData = JSONUtil.toBean(json, new TypeReference<RedisData<R>>() {}, false);
Object object = redisData.getData();
R r = JSONUtil.toBean((JSONObject) object, type);
LocalDateTime expireTime = redisData.getExpireTime();
//5.判断是否过期
if (expireTime.isAfter(LocalDateTime.now())){
//5.1未过期,直接返回信息
return r;
}
//5.2已过期,需要缓存重建
//6.缓存重建
//6.1获取互斥锁
String lockKey = LOCK_SHOP_KEY+id;
//主线程加锁,setnx的是主线程的id
String currentLockValue = ID_PREFIX + Thread.currentThread().getId();
boolean isLock = tryLock(lockKey,currentLockValue);
//6.2判断是否获取锁成功
if (isLock){
//需要再次检查redis缓存是否过期
//1.从redis查询商铺缓存
json = stringRedisTemplate.opsForValue().get(key);
//2.判断是否不存在
if (StrUtil.isBlank(json)) {
//3.不存在,直接返回
return null;
}
redisData = JSONUtil.toBean(json, new TypeReference<RedisData<R>>() {}, false);
// object = redisData.getData();
// r = JSONUtil.toBean((JSONObject) object, type);
expireTime = redisData.getExpireTime();
//5.判断是否过期
if (expireTime.isAfter(LocalDateTime.now())){
//5.1未过期,直接返回信息
unLock(lockKey,currentLockValue);
return r;
}
try {
//还是已过期且成功获取到锁
//6.3成功,开启独立线程,实现缓存重建,通过线程池
CACHE_REBUILD_EXECUTOR.submit(()->{
try {
//先查数据库
R r1 = dbFallBack.apply(id);
//写入redis
this.setWithLogicalExpire(key,r1,time,unit);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//一定要释放锁
unLock(lockKey,currentLockValue);
}
});
} catch (Exception e) {
// 关键改动:如果线程池提交失败(例如线程池已关闭)
// 手动释放锁,防止锁白白占用 10s TTL
unLock(lockKey, currentLockValue);
// 仅打印日志,不抛出异常,让主线程能走到下面的 return r
log.error("线程池提交任务失败, id: {}", id, e);
}
}
//6.4无论成功失败都获取缓存信息
//7.返回
return r;
}
private static final String ID_PREFIX = UUID.randomUUID().toString() + "-";
/**
* 上锁
* @param key
* @return
*/
private boolean tryLock(String key){
// 线程标识:UUID + 线程ID
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 存入 Redis 的值不再是 "1",而是这个唯一标识
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, threadId, 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
/**
* 释放锁
* @param key
*/
private void unLock(String key,String threadId){
// 1. 获取 Redis 中的锁标识
//String threadId = ID_PREFIX + Thread.currentThread().getId();
String id = stringRedisTemplate.opsForValue().get(key);
// 2. 判断标识是否一致
if (threadId.equals(id)) {
// 3. 是一致的才删除
stringRedisTemplate.delete(key);
}
}
单参数tryLock
private static final String ID_PREFIX = UUID.randomUUID().toString() + "-";
/**
* 上锁
* @param key
* @return
*/
private boolean tryLock(String key){
// 线程标识:UUID + 线程ID
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 存入 Redis 的值不再是 "1",而是这个唯一标识
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, threadId, 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
//private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
// 使用 ThreadPoolExecutor 手动创建,不再使用 Executors 快捷工厂
private static final ExecutorService CACHE_REBUILD_EXECUTOR = new ThreadPoolExecutor(
10, // 核心线程数
20, // 最大线程数
1L, TimeUnit.MINUTES, // 空闲线程存活时间
new ArrayBlockingQueue<>(100), // 有界队列:最多排队 100 个,超过就触发拒绝策略
new ThreadPoolExecutor.CallerRunsPolicy() // 关键:拒绝策略改为“调用者运行”
);
public <R,ID> R queryWithLogicalExpire(String keyPrefix,ID id,Class<R> type,Function<ID,R> dbFallBack, Long time, TimeUnit unit){
String key = keyPrefix + id;
//1.从redis查询商铺缓存
String json = stringRedisTemplate.opsForValue().get(key);
//2.判断是否不存在
if (StrUtil.isBlank(json)) {
//3.不存在,直接返回
return null;
}
//4.命中,需要先把json反序列化为对象
// RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
// JSONObject data = (JSONObject)redisData.getData();
// Shop shop = JSONUtil.toBean(data, Shop.class);//也可以用泛型,就可以直接得到Shop类
RedisData<R> redisData = JSONUtil.toBean(json, new TypeReference<RedisData<R>>() {}, false);
Object object = redisData.getData();
R r = JSONUtil.toBean((JSONObject) object, type);
LocalDateTime expireTime = redisData.getExpireTime();
//5.判断是否过期
if (expireTime.isAfter(LocalDateTime.now())){
//5.1未过期,直接返回信息
return r;
}
//5.2已过期,需要缓存重建
//6.缓存重建
//6.1获取互斥锁
String lockKey = LOCK_SHOP_KEY+id;
//主线程加锁,setnx的是主线程的id
String currentLockValue = ID_PREFIX + Thread.currentThread().getId();
boolean isLock = tryLock(lockKey);
//6.2判断是否获取锁成功
if (isLock){
//需要再次检查redis缓存是否过期
//1.从redis查询商铺缓存
json = stringRedisTemplate.opsForValue().get(key);
//2.判断是否不存在
if (StrUtil.isBlank(json)) {
//3.不存在,直接返回
return null;
}
redisData = JSONUtil.toBean(json, new TypeReference<RedisData<R>>() {}, false);
// object = redisData.getData();
// r = JSONUtil.toBean((JSONObject) object, type);
expireTime = redisData.getExpireTime();
//5.判断是否过期
if (expireTime.isAfter(LocalDateTime.now())){
//5.1未过期,直接返回信息
unLock(lockKey,currentLockValue);
return r;
}
try {
//还是已过期且成功获取到锁
//6.3成功,开启独立线程,实现缓存重建,通过线程池
CACHE_REBUILD_EXECUTOR.submit(()->{
try {
//先查数据库
R r1 = dbFallBack.apply(id);
//写入redis
this.setWithLogicalExpire(key,r1,time,unit);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//一定要释放锁
unLock(lockKey,currentLockValue);
}
});
} catch (Exception e) {
// 关键改动:如果线程池提交失败(例如线程池已关闭)
// 手动释放锁,防止锁白白占用 10s TTL
unLock(lockKey, currentLockValue);
// 仅打印日志,不抛出异常,让主线程能走到下面的 return r
log.error("线程池提交任务失败, id: {}", id, e);
}
}
//6.4无论成功失败都获取缓存信息
//7.返回
return r;
}
3.企业级CANAL,只查询缓存,将增删改操作对应的更新缓存通过mq执行。通过RabbitMq,mysql在linux里面
配置依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
配置yml(Sping下)
rabbitmq:
host: 192.168.100.128 # RabbitMQ服务器地址
port: 5672 # 端口
username: root # 账号
password: 1234 # 密码
virtual-host: / # 虚拟主机
listener:
package com.hmdp.listener;
import cn.hutool.core.lang.TypeReference;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.CanalBean;
import com.hmdp.utils.RedisData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import static com.hmdp.utils.RedisConstants.CACHE_SHOP_KEY;
import static com.hmdp.utils.RedisConstants.CACHE_SHOP_TTL;
@Slf4j
@Configuration
public class CanalTb_ShopListener {
@Resource
private StringRedisTemplate stringRedisTemplate;
@RabbitListener(queues = "canal.queue")
public void onMessage(String message){
// 1. 将 message 字符串直接解析为 CanalBean
// 这里指定 T 为 JSONObject,方便后续灵活处理
CanalBean<JSONObject> canalBean =
JSONUtil.toBean(message, new TypeReference<CanalBean<JSONObject>>() {}, false);
String type = canalBean.getType();
String table = canalBean.getTable();
if (!"tb_shop".equals(table)){
return;
}
if ("DELETE".equals(type)){
canalBean.getData().forEach(data->
stringRedisTemplate.delete(CACHE_SHOP_KEY+data.get("id")));
}else {
canalBean.getData().forEach(data->
{
String key = CACHE_SHOP_KEY+data.get("id");
RedisData<JSONObject> redisData = RedisData.<JSONObject>builder()
.data(data)
.expireTime(LocalDateTime.now().plusMinutes(CACHE_SHOP_TTL))
.build();
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));
log.info("Canal自动同步缓存成功,key: {}", key);
});
}
}
}

浙公网安备 33010602011771号