功能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的快取,實現資料的一致性。