功能02-商铺查询缓存3.商铺详情缓存查询3.1什么是缓存?

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

缓存的作用:

  1. 降低后端负载
  2. 提高读写效率,降低响应时间

缓存的成本:

  1. 数据一致性成本
  2. 代码维护成本
  3. 运维成本

3.2需求说明

如下,当我们点击商店详情的时候,前端会向后端发出请求,后端需要把相关的商店数据返回给客户端显示。

3.3思路分析(添加Redis缓存)

使用Redis的缓存模型如下:

当客户端发送请求到服务端时,先去redis中查询有没有对应的数据:

  • 如果命中,则直接给客户端返回数据,这样直接访问数据库的请求就会大大减少
  • 如果未命中,则到数据库中查询,同时将数据写入redis,防止下一次查询同样的数据,然后将数据返回给客户端

3.4代码实现

(1)Shop.java 实体类

package com.hmdp.entity;import com.baomidou.mybatisplus.annotation.IdType;import com.baomidou.mybatisplus.annotation.TableField;import com.baomidou.mybatisplus.annotation.TableId;import com.baomidou.mybatisplus.annotation.TableName;import lombok.Data;import lombok.EqualsAndHashCode;import lombok.experimental.Accessors;import java.io.Serializable;import java.time.LocalDateTime;/** * @author 李 * @version 1.0 */@Data@EqualsAndHashCode(callSuper = false)@Accessors(chain = true)@TableName("tb_shop")public class Shop implements Serializable {    private static final long serialVersionUID = 1L;    //主键    @TableId(value = "id", type = IdType.AUTO)    private Long id;    //商铺名称    private String name;    //商铺类型id    private Long typeId;    //商铺图片,多个图片以','隔开    private String images;    //商圈,例如陆家嘴    private String area;    //地址    private String address;    //经度    private Double x;    //纬度    private Double y;    //均价,取整数    private Long avgPrice;    //销量    private Integer sold;    //评论数量    private Integer comments;    //评分,1~5分,乘10保存,避免小数    private Integer score;    //营业时间,例如 10:00-22:00    private String openHours;    //创建时间    private LocalDateTime createTime;    //更新时间    private LocalDateTime updateTime;    @TableField(exist = false)    private Double distance;}

(2)对应的mapper接口

package com.hmdp.mapper;import com.hmdp.entity.Shop;import com.baomidou.mybatisplus.core.mapper.BaseMapper;/** * Mapper 接口 * * @author 李 * @version 1.0 */public interface ShopMapper extends BaseMapper {}

(3)IShopService.java 接口

package com.hmdp.service;import com.hmdp.dto.Result;import com.hmdp.entity.Shop;import com.baomidou.mybatisplus.extension.service.IService;/** *  服务类 * * @author 李 * @version 1.0 */public interface IShopService extends IService {    Result queryById(Long id);}

(4)ShopServiceImpl 服务实现类

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 static com.hmdp.utils.RedisConstants.*;/** *  * 服务实现类 * * @author 李 * @version 1.0 */@Servicepublic class ShopServiceImpl extends ServiceImpl implements IShopService {    @Resource    StringRedisTemplate stringRedisTemplate;    @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)) {            //2.1若命中,直接返回商铺信息            Shop shop = JSONUtil.toBean(shopJson, Shop.class);            return Result.ok(shop);        }        //2.2未命中,根据id查询数据库,判断商铺是否存在数据库中        Shop shop = getById(id);        if (shop == null) {            //2.2.1不存在,则返回404            return Result.fail("店铺不存在!");        }        //2.2.2存在,则将商铺数据写入redis中        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop));        return Result.ok(shop);    }}

(5)ShopController 控制类

package com.hmdp.controller;import cn.hutool.core.util.StrUtil;import com.baomidou.mybatisplus.extension.plugins.pagination.Page;import com.hmdp.dto.Result;import com.hmdp.entity.Shop;import com.hmdp.service.IShopService;import com.hmdp.utils.SystemConstants;import org.springframework.web.bind.annotation.*;import javax.annotation.Resource;/** * 前端控制器 * * @author 李 * @version 1.0 */@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 shopService.queryById(id);    }}

(6)测试:首次查询的时候因为数据为写入reids,因此查询较慢,第二次因为已写入redis,查询较快

4.商铺类型缓存查询4.1需求说明

店铺类型在首页和其他多个页面都会用到,如下:

要求当我们点击商铺类型的时候,前端会向后端发出请求,后端需要把相关的商店类型数据返回给客户端显示:

4.2思路分析

该功能的实现思路与上述的思路大体一致。

4.3代码实现

(1)实体类 ShopType

package com.hmdp.entity;import com.baomidou.mybatisplus.annotation.IdType;import com.baomidou.mybatisplus.annotation.TableId;import com.baomidou.mybatisplus.annotation.TableName;import com.fasterxml.jackson.annotation.JsonIgnore;import lombok.Data;import lombok.EqualsAndHashCode;import lombok.experimental.Accessors;import java.io.Serializable;import java.time.LocalDateTime;/** * @author 李 * @version 1.0 */@Data@EqualsAndHashCode(callSuper = false)@Accessors(chain = true)@TableName("tb_shop_type")public class ShopType implements Serializable {    private static final long serialVersionUID = 1L;    //主键    @TableId(value = "id", type = IdType.AUTO)    private Long id;    //类型名称    private String name;    //图标    private String icon;    //顺序    private Integer sort;    //创建时间    @JsonIgnore    private LocalDateTime createTime;    //更新时间    @JsonIgnore    private LocalDateTime updateTime;}

(2)ShopTypeMapper接口

package com.hmdp.mapper;import com.hmdp.entity.ShopType;import com.baomidou.mybatisplus.core.mapper.BaseMapper;/** * Mapper 接口 * * @author 李 * @version 1.0 */public interface ShopTypeMapper extends BaseMapper {}

(3)服务类接口 IShopTypeService

package com.hmdp.service;import com.hmdp.dto.Result;import com.hmdp.entity.ShopType;import com.baomidou.mybatisplus.extension.service.IService;/** * 服务类接口 * * @author 李 * @version 1.0 */public interface IShopTypeService extends IService {    Result queryShopList();}

(4)服务实现类 ShopTypeServiceImpl

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.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 javax.annotation.Resource;import java.util.ArrayList;import java.util.List;import static com.hmdp.utils.RedisConstants.CACHE_SHOP_TYPE;/** * 服务实现类 * * @author 李 * @version 1.0 */@Servicepublic class ShopTypeServiceImpl extends ServiceImpl implements IShopTypeService {    @Resource    private StringRedisTemplate stringRedisTemplate;    @Override    public Result queryShopList() {        //查询redis中有没有店铺类型缓存        String shopTypeJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_TYPE);        //如果有,则将其转为对象类型,并返回给客户端        if (StrUtil.isBlank(shopTypeJson)) {            List shopTypeList = JSONUtil.toList(shopTypeJson, ShopType.class);            return Result.ok(shopTypeList);        }        //如果redis中没有缓存,到DB中查询        //如果DB中没有查到,返回错误信息        List list = query().orderByAsc("sort").list();        if (list == null) {            return Result.fail("查询不到店铺类型!");        }        //如果DB查到了数据        //将数据存入Redis中(转为json类型存入)        stringRedisTemplate.opsForValue()                .set(CACHE_SHOP_TYPE, JSONUtil.toJsonStr(list));        //并返回给客户端        return Result.ok(list);    }}

(5)控制类 ShopTypeController

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;/** * 前端控制器 * * @author 李 * @version 1.0 */@RestController@RequestMapping("/shop-type")public class ShopTypeController {    @Resource    private IShopTypeService typeService;    @GetMapping("list")    public Result queryTypeList() {        return typeService.queryShopList();    }}

(6)测试,访问客户端首页,

返回的数据如下:

5.缓存更新5.1缓存更新策略5.1.1主动更新策略

  • Cache Aside Pattern:由缓存的调用者,在更新数据库的同时更新缓存(可控性最高,推荐使用)
  • Read/Write Through Pattern:缓存与数据库整合为一个服务,由服务来维护一致性。调用者调用该服务,无需关心缓存一致性问题
  • Write Behind Caching Pattern:调用者只操作缓存,由其他线程异步的将缓存数据持久化到数据库,保证最终一致

操作缓存和数据库时有三个问题需要考虑:

  1. 删除缓存还是更新缓存?

    • 更新缓存:每次更新数据库都更新缓存,无效写操作较多
    • 删除缓存:更新数据库时让缓存失效,查询时再更新缓存(推荐使用)
  2. 如何保证缓存与数据库的操作的同时成功或失败?(原子性)

    • 单体系统,将缓存与数据库操作放在一个事务
    • 分布式系统,利用TCC等分布式事务方案
  3. 先操作缓存还是先操作数据库?(线程安全问题)

    如上,虽然两种方案都有可能造成缓存和数据库不一致,但更推荐先更新数据库再删除缓存。

    先更新数据库再删除缓存出现数据不一致概率更低,因为操作缓存一般比数据库更快,所以发生右图的情况很低(右图)。即使发生了,可以配合TTL定时清除缓存。

5.1.2总结

缓存更新策略的最佳实践方案:

  1. 低一致性需求:使用Redis自带的内存淘汰机制即可
  2. 高一致性需求:主动更新,并以超时剔除作为兜底方案
    • 读操作:
      • 缓存命中则直接返回
      • 缓存未命中则查询数据库,并写入缓存,设定超时时间
    • 写操作:
      • 先写数据库,然后再删除缓存
      • 要确保数据库与缓存操作的原子性

5.2需求说明

给查询商铺的缓存添加超时剔除和主动更新策略:

  1. 根据id查询店铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间
  2. 根据id修改店铺,先修改数据库,再删除缓存

5.3代码实现

(1)修改ShopServiceImpl的queryById()方法,设置超时时间

并添加update()方法如下:

@Override@Transactionalpublic Result update(Shop shop) {    Long id = shop.getId();    if (id == null) {        return Result.fail("店铺id不能为空");    }    //1.更新数据库    updateById(shop);    //2.删除redis缓存    stringRedisTemplate.delete(CACHE_SHOP_KEY + id);    return Result.ok();}

(2)修改IShopService,添加方法声明

Result update(Shop shop);

(3)修改ShopController,添加方法

/** * 更新商铺信息 * @param shop 商铺数据 * @return 无 */@PutMappingpublic Result updateShop(@RequestBody Shop shop) {    // 写入数据库    return shopService.update(shop);}

(4)测试

读操作:首次访问店铺详情,可以看到redis中存入数据,并且设置了TTL

写操作:使用postman向服务端发送更新店铺信息请求,可以看到当更新数据时候,先更新数据库,然后将redis的缓存删除。之后如果再有查询,将会重建redis的缓存,实现数据的一致性。