Day77(3)-F:\code\hm-dianping\hm-dianping\src\main\java\com\hmdp\service\impl\ShopServiceImpl.java-redis基础

黑马点评

商户查询缓存

image-20260109131651789

image-20260109131846696

添加Redis缓存

image-20260109132445359

根据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);
}

image-20260109134433012

查询商铺种类信息-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);
    }
}

缓存更新策略

image-20260109141631205

image-20260109142037510

image-20260109142330764

image-20260109143233705

image-20260109143240838

技术栈

  1. 写数据:先写数据库再删缓存;读数据:读就先看缓存有没有,如果有就返回,没有再去查数据库,并写入缓存,设置超时时间
  2. 更新数据,先改数据库,再删除缓存
1.写数据:先写数据库再删缓存;读数据:读就先看缓存有没有,如果有就返回,没有再去查数据库,并写入缓存,设置超时时间

image-20260109144019256

读数据设置超时时间(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();
}

缓存穿透

image-20260109151004076

知识点

布隆过滤原理
  1. 底层核心组成
    布隆过滤器的底层主要由两部分组成:
    • 位数组(Bit Array):一个长度为 的位数组,初始状态下所有位都设置为 0。
    • 哈希函数(Hash Functions): 个独立的、且能够将输入均匀分布的无偏哈希函数。
  2. 工作原理:添加与查询
    A. 元素写入(Add)
    当我们要向布隆过滤器中添加一个元素(例如:用户 ID "123")时:
  3. 分别使用 个哈希函数对该元素进行计算,得到 个哈希值。
  4. 将这 个哈希值对位数组长度 取模,得到 个在 范围内的索引位置。
  5. 将位数组中对应的这 个位置的值全部置为 1。
    B. 元素查询(Query)
    当我们要查询一个元素是否存在时:
  6. 同样使用那 个哈希函数计算出 个索引位置。
  7. 检查位数组中这 个位置的值:
    • 只要有一个位置为 0:该元素一定不存在于集合中。
    • 所有位置都为 1:该元素可能存在(也可能是因为其他多个元素刚好把这些位置都占满了,产生了“哈希碰撞”)。
  8. 为什么会产生误判?
    误判(False Positive)是布隆过滤器的天性。 假设元素 A 占用了位置 1, 3, 5,元素 B 占用了位置 2, 4, 6。此时我们要查询元素 C,如果 C 的哈希计算结果刚好是 1, 4, 6,你会发现这三个位置都是 1,于是布隆过滤器会告诉你“C 存在”,但实际上 C 从未被添加过。
    影响误判率的因素:
    • 位数组长度 :数组越大,位被占满的速度越慢,误判率越低。
    • 哈希函数个数 :函数太少容易碰撞;函数太多会导致位数组迅速变满。
    • 存储元素个数 :存入的元素越多,误判率越高。

image-20260109151642548

技术栈

  1. 通过数据库向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);
}

image-20260109154221101

知识点

通过雪花id和后端给前端返回时再加密保护数据库不被攻击

HashID:这是目前很流行的做法。数据库内部依然用自增 ID 或雪花 ID,但在返回给前端接口时,通过算法将其加密成一段看起来像乱码的字符串(如 jRwl8Y9)。

缓存雪崩

image-20260109154851508

利用集群(高级板块)

降级限流(springcloud)

缓存击穿

image-20260109155339360

image-20260109155932118

image-20260109160232207

image-20260109161034951

知识点

目前主流的方案主要分为两派:基于 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)。

设计逻辑:
  1. 临时顺序节点:每个请求进来,在 ZK 的锁路径下创建一个带序号的临时节点。
  2. 最小序号获胜:谁的序号最小,谁就获得锁。
  3. 监听机制(Watch):没拿到锁的线程不进行死循环重试,而是监听前一个节点的删除事件。前一个节点一旦消失(业务完成或崩溃),ZK 会立即通知下一个节点。

3. 企业设计中的“大坑”:主从切换下的锁丢失

这是面试常问,也是架构设计必须考虑的问题。

  • 场景:你在 Redis 主节点加了锁,主节点还没来得及把数据同步给从节点就挂了。从节点升级为主节点,此时你的锁丢了,另一个线程进来又能加锁成功。
  • 企业对策
    • Redlock(红锁)算法:向 5 个独立的 Redis 实例请求加锁,只有超过半数(3个)成功才算成功。虽然安全,但性能损耗大,目前争议较大。
    • 业务补偿/幂等设计:大部分企业接受极小概率的锁丢失,但在数据库层面通过 CAS(乐观锁)唯一索引 做最终兜底,确保即便锁失效,数据也不会乱。

技术栈

  1. 互斥锁解决
  2. 逻辑过期解决
  3. 调用线程池中的线程进行线程重建(不要去新建一个线程)
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();
    }
}

image-20260109171610633

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)。

缓存工具类封装

image-20260109204007787

技术栈

  1. 泛型函数抽取
  2. 拆包类型转换
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);
    }
}

补充技术

  1. 自定义线程池,不用系统默认的无界线程池,提高性能
  2. 锁标识传递 ,主线程把自己的线程id传给重建缓存的线程执行
  3. 企业级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);
            });
        }

    }

}
posted @ 2026-01-11 15:32  David大胃  阅读(2)  评论(0)    收藏  举报