功能02-商鋪查詢緩存 3.商鋪詳情緩存查詢 3.1什麼是緩存? 緩存就是數據交換的緩衝區(稱作Cache),是存儲數據的臨時地方,一般讀寫性能較高。 緩存的作用: 降低後端負載 提高讀寫效率,降低響應時間 緩存的成本: 數據一致性成本 代碼維護成本 運維成本 3.2需求說明 如下,當我們點擊商店詳 ...
功能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的緩存,實現數據的一致性。