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

本文目录

  • Redis 实战 —— 实战篇
    • Business Search Cache —— 商业查询缓存
      • 什么是缓存
      • 添加 Redis 缓存
        • ❓ 为什么 使用BeanUtil做beanToMap时,转换字段(可能为null)的属性为String类型
        • ⭐ 使用 Redis Hash 进行存储数据
        • ⭐ 使用 Redis String 进行存储数据

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 —— 商业查询缓存

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

什么是缓存

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

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

缓存的作用

1️⃣ 降低后端负载

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

缓存的成本

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

2️⃣ 代码维护成本

3️⃣ 运维成本

添加 Redis 缓存

查询商户的业务流程

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

根据请求查询对应的数据

@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));
    }
}

缓存作用模型

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

添加缓存的业务逻辑思路

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

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

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

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

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

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

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

此时已经缓存到了数据库

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

在这里插入图片描述

此时就发现我们之前缓存好的数据只需要 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);
}

你可能感兴趣的:(Redis,实战与原理,redis,缓存,数据库)