功能02-商铺查询缓存
3.商铺详情缓存查询
3.1什么是缓存?
缓存就是数据交换的缓冲区(称作Cache),是存储数据的临时地方,一般读写性能较高。
缓存的作用:
缓存的成本:
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<Shop> {
- }
复制代码 (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<Shop> {
- 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
- */
- @Service
- public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> 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<ShopType> {
- }
复制代码 (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<ShopType> {
- 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
- */
- @Service
- public class ShopTypeServiceImpl extends ServiceImpl<ShopTypeMapper, ShopType> implements IShopTypeService {
- @Resource
- private StringRedisTemplate stringRedisTemplate;
- @Override
- public Result queryShopList() {
- //查询redis中有没有店铺类型缓存
- String shopTypeJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_TYPE);
- //如果有,则将其转为对象类型,并返回给客户端
- if (StrUtil.isBlank(shopTypeJson)) {
- List<ShopType> shopTypeList = JSONUtil.toList(shopTypeJson, ShopType.class);
- return Result.ok(shopTypeList);
- }
- //如果redis中没有缓存,到DB中查询
- //如果DB中没有查到,返回错误信息
- List<ShopType> 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:调用者只操作缓存,由其他线程异步的将缓存数据持久化到数据库,保证最终一致
操作缓存和数据库时有三个问题需要考虑:
- 删除缓存还是更新缓存?
- 更新缓存:每次更新数据库都更新缓存,无效写操作较多
- 删除缓存:更新数据库时让缓存失效,查询时再更新缓存(推荐使用)
- 如何保证缓存与数据库的操作的同时成功或失败?(原子性)
- 单体系统,将缓存与数据库操作放在一个事务
- 分布式系统,利用TCC等分布式事务方案
- 先操作缓存还是先操作数据库?(线程安全问题)
如上,虽然两种方案都有可能造成缓存和数据库不一致,但更推荐先更新数据库再删除缓存。
先更新数据库再删除缓存出现数据不一致概率更低,因为操作缓存一般比数据库更快,所以发生右图的情况很低(右图)。即使发生了,可以配合TTL定时清除缓存。
5.1.2总结
缓存更新策略的最佳实践方案:
- 低一致性需求:使用Redis自带的内存淘汰机制即可
- 高一致性需求:主动更新,并以超时剔除作为兜底方案
- 读操作:
- 缓存命中则直接返回
- 缓存未命中则查询数据库,并写入缓存,设定超时时间
- 写操作:
- 先写数据库,然后再删除缓存
- 要确保数据库与缓存操作的原子性
5.2需求说明
给查询商铺的缓存添加超时剔除和主动更新策略:
- 根据id查询店铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间
- 根据id修改店铺,先修改数据库,再删除缓存
5.3代码实现
(1)修改ShopServiceImpl的queryById()方法,设置超时时间
并添加update()方法如下:- @Override
- @Transactional
- public 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 无
- */
- @PutMapping
- public Result updateShop(@RequestBody Shop shop) {
- // 写入数据库
- return shopService.update(shop);
- }
复制代码 (4)测试
读操作:首次访问店铺详情,可以看到redis中存入数据,并且设置了TTL
写操作:使用postman向服务端发送更新店铺信息请求,可以看到当更新数据时候,先更新数据库,然后将redis的缓存删除。之后如果再有查询,将会重建redis的缓存,实现数据的一致性。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |