Redis —— Redis In Action —— Redis 实战—— 实战篇二 —— 商业查询缓存 —— Redis 作为缓存数据返回给用户 — 有代码

📖 Redis 实战 —— 实战篇

在这里插入图片描述

课程介绍

heima 点评Redis —— 项目

1️⃣ 短信登录 —— Redis 的共享 session 应用

2️⃣ 商户查询缓存 —— 企业的缓存使用技巧| 缓存雪崩、穿透等问题解决

3️⃣ 达人探店 —— 基于 List 点赞链表|基于 SortedSet 的点赞排行榜

4️⃣ 优惠券秒杀 —— Redis 的计数器| Lua 脚本 Redis | 分布式锁 | Redis 的三种消息队列 ⭐

5️⃣ 好友关注 —— 基于 Set 集合的关注|取关|共同关注|消息推送的功能

6️⃣ 附近的商户 —— Redis 的 GeoHash 的应用

7️⃣ 用户签到 —— Redis 的 BitMap 数据统计功能

8️⃣ UV 统计 —— Redis 的 HyperLogLog 的统计功能

📑 Business Search Cache —— 商业查询缓存

在这里插入图片描述

🔖 什么是缓存

缓存就是数据交换的缓冲区(称之为 Cache),是存储数据的临时地方,一般读写性能较高。

在这里插入图片描述

缓存的作用

1️⃣ 降低后端负载

2️⃣ 提高读写效率,降低响应时间

缓存的成本

1️⃣ 数据的一致性成本问题

2️⃣ 代码维护成本

3️⃣ 运维成本

🔖 添加 Redis 缓存

查询商户的业务流程

在这里插入图片描述

根据请求查询对应的数据

@RestController
@RequestMapping("/shop")
public class ShopController {
    @Resource
    public IShopService shopService;
    /**
     * 根据id查询商铺信息
     * @param id 商铺id
     * @return 商铺详情数据
     */
    @GetMapping("/{id}")
    public Result queryShopById(@PathVariable("id") Long id) {
        return Result.ok(shopService.getById(id));
    }
}

缓存作用模型

在这里插入图片描述

添加缓存的业务逻辑思路

在这里插入图片描述

1️⃣ 当用户进行提交查询商铺时的请求时 会携带商铺的 id 信息

2️⃣ 尝试 通过 商铺 id 信息 到缓存中查看是否存在对应缓存信息

  • 1.如果未命中缓存,则根据 商铺 id 到数据库中查询对应的数据
    • 存在商铺数据——将其商铺数据写入 Redis 当中,然后返回商铺信息
    • 不存在商铺数据——返回 404 状态码
  • 2.如果命中缓存,则直接返回商铺对应的信息。

3️⃣ 结束

❓ 为什么 使用BeanUtil做beanToMap时,转换字段(可能为null)的属性为String类型

这里小付采用 Redis Hash 将数据存入 redis中

【参考资料】 【运行报错】使用BeanUtil做beanToMap时,转换字段(可能为null)的属性为String类型

1️⃣ 报错信息

  1. 使用Redis代替session做缓存时,需要将redis中未命中的数据,从数据库查出再存入redis缓存
  2. 首先得将对象转为hashmap(这里使用得是hutool的BeanUtil)
  3. 且使用StringRedisTemplateredis需要转换成的map的各个字段都是String类型!而bean的各个字段类型各不相同
  4. 如何在不遍历map(繁琐)的情况下,在转换为map时就直接将字段类型也转换为String?
Map<String, Object> map = BeanUtil.beanToMap(shop,new HashMap<>(),
        CopyOptions.create().
                setIgnoreNullValue(true)
                .setFieldValueEditor((fieldName,fieldValue) -> fieldValue.toString()));
stringRedisTemplate.opsForHash().putAll(RedisConstants.CACHE_SHOP_KEY + id,map);
  1. 但是执行报错了
java.lang.NullPointerException: null
at com.hmdp.service.impl.ShopServiceImpl.lambda$queryById$0(ShopServiceImpl.java:81) ~[classes/:na]
at cn.hutool.core.bean.copier.CopyOptions.editFieldValue(CopyOptions.java:258) ~[hutool-all-5.7.17.jar:na]
at cn.hutool.core.bean.copier.BeanCopier.lambda$beanToMap$1(BeanCopier.java:233) ~[hutool-all-5.7.17.jar:na]
at java.util.LinkedHashMap$LinkedValues.forEach(LinkedHashMap.java:608) ~[na:1.8.0_181]
at cn.hutool.core.bean.BeanUtil.descForEach(BeanUtil.java:182) ~[hutool-all-5.7.17.jar:na]
at cn.hutool.core.bean.copier.BeanCopier.beanToMap(BeanCopier.java:195) ~[hutool-all-5.7.17.jar:na]
at cn.hutool.core.bean.copier.BeanCopier.copy(BeanCopier.java:106) ~[hutool-all-5.7.17.jar:na]
at cn.hutool.core.bean.BeanUtil.beanToMap(BeanUtil.java:690) ~[hutool-all-5.7.17.jar:na]
  1. 空指针异常

2️⃣ 原因分析

  1. 发现需要转成map得bean中有字段是null
  2. null不能toString()
  3. 故将代码修改为:
Map<String, Object> map = BeanUtil.beanToMap(shop,new HashMap<>(),
    CopyOptions.create().
            setIgnoreNullValue(true)
            .setFieldValueEditor((fieldName,fieldValue) -> fieldValue + ""));
stringRedisTemplate.opsForHash().putAll(RedisConstants.CACHE_SHOP_KEY + id,map);
  1. 但还是存在问题:

  2. 现在空值是以字符串 “null” 存在redis中的

  3. 当再次点击此商铺,此时缓存命中,故将redis中的hash类型数据转为 shop 对象时,“null” 又无法将string类型的value解析成一个数字类型

  4. 此时注意到,我代码中明明将 空值忽略掉了

    setIgnoreNullValue(true)

  5. 但debug却发现setIgnoreNullValue(true)并没有 生效

3️⃣ 解决方法

  1. setFieldValueEditor 优先级要高于 ignoreNullValue导致前者首先被触发,因此出现空指针问题。你在setFieldValueEditor中也需要判空
  2. 这么设计的原因主要是,如果原值确实是null,但是你想给一个默认值,在此前过滤掉就不合理了,而你的值编辑后转换为null,后置的判断就会过滤掉
  3. 修改代码:
// todo 3.2 判断商铺缓存是否存在 => 存在 将商铺数据写入到 Redis 中 => 返回 商铺信息
Map<String, Object> redisCache = BeanUtil.beanToMap(shop,new HashMap<>()
,CopyOptions.create()
        .setIgnoreNullValue(true)
        .setFieldValueEditor((k,v)->{
            if (k == null){
                v = "0";
            }else {
                v = v + "";
            }
            return v;
        }));
stringRedisTemplate.opsForHash().putAll(shopKey , redisCache);
return Result.ok(redisCache);
  1. 问题解决

⭐ 使用 Redis Hash 进行存储数据

步骤一:改写 Service 层业务逻辑

ShopController

/**
 * 根据id查询商铺信息
 * @param id 商铺id
 * @return 商铺详情数据
 */
@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {
    return shopService.queryById(id);
}

IShopService

public interface IShopService extends IService<Shop> {
    
    Result queryById(Long id);
}

ShopServiceImpl

@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
    @Autowired
    StringRedisTemplate stringRedisTemplate ;
    
    @Override
    public Result queryById(Long id) {
        // todo 1. 根据商铺 id 到 Redis 中查询商铺缓存
        String shopKey = RedisConstants.CACHE_SHOP_KEY + id ;
        Map<Object, Object> shopMap = stringRedisTemplate.opsForHash().entries(shopKey);
        // todo 2. 缓存命中 => 返回商铺信息
        if (shopMap.isEmpty()){
            // todo 3. 缓存未命中 => 根据 id 查询数据库
            Shop shop = getById(id);
            // todo 3.1 判断商铺缓存是否存在 => 不存在 返回404状态码 提示错误信息
            if (shop == null){
                return Result.fail("404 您访问的商户不存在!");
            }
            // todo 3.2 判断商铺缓存是否存在 => 存在 将商铺数据写入到 Redis 中 => 返回 商铺信息
            Map<String, Object> redisCache = BeanUtil.beanToMap(shop,new HashMap<>()
            ,CopyOptions.create()
                    .setIgnoreNullValue(true)
                    .setFieldValueEditor((k,v)->{
                        if (k == null){
                            v = "0";
                        }else {
                            v = v + "";
                        }
                        return v;
                    }));
            stringRedisTemplate.opsForHash().putAll(shopKey , redisCache);
            return Result.ok(redisCache);
        }
        // todo 4 结束
        
        return Result.ok(shopMap);
        
    }
}

步骤二:启动应用程序进行业务逻辑测试

在这里插入图片描述

1️⃣ 第一次查询还是走了数据库的查询 所以 时间为 153 ms

查看对应 Redis 中是否已经缓存好了对应的数据

在这里插入图片描述

📚 此时已经缓存到了数据库

此时我们再次去进行查询之前的数据查看请求时间

在这里插入图片描述

此时就发现我们之前缓存好的数据只需要 16 ms 就可以完成请求了 快了 将近 10倍

反观IDEA控制台中并没有对应的 SQL 查询日志,故我们的缓存生效 极大提高了效率。

⭐ 使用 Redis String 进行存储数据

步骤一:这里只提供对应的Controller 层代码的编写对应的业务逻辑要求

/**
 * 功能描述
 * 商业缓存的具体实现 以 String 类型传入
 * @date 2022/7/10
 * @author Alascanfu
 */
@Override
public Result cacheStringQueryById(Long id) {
    // todo 1. 根据商铺 id 到 Redis 中查询商铺缓存
    String shopKey = RedisConstants.CACHE_SHOP_KEY + id ;
    // todo 2. 缓存命中 => 返回商铺信息
    String shopJson = stringRedisTemplate.opsForValue().get(shopKey);
    if (StrUtil.isNotBlank(shopJson)){
        Shop shop = JSONUtil.toBean(shopJson , Shop.class);
        return Result.ok(shop) ;
    }
    // todo 3. 缓存未命中 => 根据 id 查询数据库
    Shop shop = getById(id);
    if (shop == null){
        // todo 3.1 判断商铺缓存是否存在 => 不存在 返回404状态码 提示错误信息
        return Result.fail("404 您访问的商户不存在!");
    }
    // todo 3.2 判断商铺缓存是否存在 => 存在 将商铺数据写入到 Redis 中 => 返回 商铺信息
    stringRedisTemplate.opsForValue().set(shopKey,JSONUtil.toJsonStr(shopJson));
    return Result.ok(shop);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Alascanfu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值