最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

SpringBoot實(shí)現(xiàn)緩存與數(shù)據(jù)庫(kù)雙寫策略的詳細(xì)代碼

 更新時(shí)間:2026年04月22日 08:45:19   作者:希望永不加班  
在SpringBoot企業(yè)開發(fā)中,為了提升系統(tǒng)性能,我們都會(huì)給高頻查詢接口加上緩存,把熱點(diǎn)數(shù)據(jù)緩存起來,減少數(shù)據(jù)庫(kù)查詢壓力,因此本文給大家介紹了SpringBoot實(shí)現(xiàn)緩存與數(shù)據(jù)庫(kù)雙寫策略的詳細(xì)方法,需要的朋友可以參考下

引言

在SpringBoot企業(yè)開發(fā)中,為了提升系統(tǒng)性能,我們都會(huì)給高頻查詢接口加上緩存(比如Redis、Caffeine),把熱點(diǎn)數(shù)據(jù)緩存起來,減少數(shù)據(jù)庫(kù)查詢壓力,讓接口響應(yīng)速度從幾十毫秒提升到幾毫秒。

但緩存的引入,也帶來了一個(gè)核心難題——緩存一致性:當(dāng)數(shù)據(jù)庫(kù)中的數(shù)據(jù)發(fā)生修改(新增、更新、刪除)時(shí),緩存中的數(shù)據(jù)如果沒有及時(shí)同步,就會(huì)出現(xiàn)“緩存數(shù)據(jù)與數(shù)據(jù)庫(kù)數(shù)據(jù)不一致”的問題,導(dǎo)致用戶查詢到舊數(shù)據(jù)、錯(cuò)誤數(shù)據(jù),引發(fā)業(yè)務(wù)異常。

舉個(gè)真實(shí)場(chǎng)景:用戶修改了自己的昵稱,數(shù)據(jù)庫(kù)中的昵稱已經(jīng)更新,但緩存中還是舊昵稱,用戶再次查詢個(gè)人信息時(shí),看到的還是舊昵稱,體驗(yàn)極差;更嚴(yán)重的是,訂單狀態(tài)更新后緩存未同步,可能導(dǎo)致運(yùn)營(yíng)人員誤判訂單狀態(tài),造成損失。

很多同學(xué)一開始處理緩存,只懂“查詢時(shí)查緩存,沒有就查數(shù)據(jù)庫(kù)再存緩存”(即Cache-Aside策略),但忽略了數(shù)據(jù)修改時(shí)的緩存同步,導(dǎo)致緩存一致性問題頻發(fā)。

一、緩存一致性的核心問題

想要解決緩存一致性問題,首先要明白:?jiǎn)栴}的根源不是“緩存”或“數(shù)據(jù)庫(kù)”本身,而是數(shù)據(jù)修改時(shí),緩存與數(shù)據(jù)庫(kù)的操作順序、同步時(shí)機(jī),以及“并發(fā)場(chǎng)景下的競(jìng)態(tài)條件”。

1. 雙寫順序與并發(fā)競(jìng)態(tài)

當(dāng)數(shù)據(jù)發(fā)生修改時(shí),我們需要同時(shí)操作“數(shù)據(jù)庫(kù)”和“緩存”,但這兩個(gè)操作無法做到“原子性”(要么同時(shí)成功,要么同時(shí)失敗),因此會(huì)出現(xiàn)兩種核心問題:

  • 雙寫順序錯(cuò)誤:比如先更新緩存、再更新數(shù)據(jù)庫(kù),若更新數(shù)據(jù)庫(kù)失敗,緩存中是新數(shù)據(jù),數(shù)據(jù)庫(kù)中是舊數(shù)據(jù),導(dǎo)致不一致;
  • 并發(fā)競(jìng)態(tài)問題:比如一個(gè)更新操作(改數(shù)據(jù)庫(kù)+刪緩存)和一個(gè)查詢操作(查緩存+查數(shù)據(jù)庫(kù))并發(fā)執(zhí)行,查詢操作可能在更新操作刪除緩存后、更新數(shù)據(jù)庫(kù)前,查詢到舊數(shù)據(jù)并重新寫入緩存,導(dǎo)致緩存一直是舊數(shù)據(jù)。

2. 緩存一致性的目標(biāo)

我們追求的緩存一致性,不是“絕對(duì)一致性”(成本極高,沒必要),而是最終一致性:在合理的時(shí)間范圍內(nèi)(比如1秒內(nèi)),緩存數(shù)據(jù)能同步為數(shù)據(jù)庫(kù)的最新數(shù)據(jù),滿足業(yè)務(wù)需求即可。

比如用戶修改昵稱后,100毫秒內(nèi)緩存同步更新,用戶再次查詢就能看到新昵稱,這種“最終一致性”完全能滿足絕大多數(shù)業(yè)務(wù)場(chǎng)景,且實(shí)現(xiàn)成本低、性能影響小。

面試必背總結(jié):緩存一致性的核心是“解決雙寫順序和并發(fā)競(jìng)態(tài)問題”,企業(yè)級(jí)落地優(yōu)先追求“最終一致性”,而非“絕對(duì)一致性”,平衡性能與數(shù)據(jù)準(zhǔn)確性。

二、三大主流雙寫策略

目前業(yè)界解決緩存一致性的雙寫策略主要有3種,各有優(yōu)缺點(diǎn)和適用場(chǎng)景,沒有最優(yōu)方案,只有最適合業(yè)務(wù)的方案,下面逐一拆解,包含實(shí)現(xiàn)代碼、細(xì)節(jié)說明,直接復(fù)制就能用。

前置準(zhǔn)備:SpringBoot 2.7.x + Redis + Spring Cache(簡(jiǎn)化緩存操作),核心依賴如下(已包含Spring Cache和Redis整合):

<!-- SpringBoot Web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency><!-- Spring Cache 核心依賴 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Redis 依賴(分布式緩存) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Caffeine 依賴(單機(jī)緩存,可選) -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>3.1.2</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

基礎(chǔ)配置(application.yml):

spring:
  # Redis 配置(分布式緩存)
  redis:
    host: localhost
    port: 6379
    password: 123456
    database: 0
    lettuce:
      pool:
        maximum-pool-size: 10
        minimum-idle: 2
  # 緩存配置
  cache:
    type: redis # 默認(rèn)使用Redis緩存(單機(jī)可改為caffeine)
    redis:
      time-to-live: 3600000 # 緩存過期時(shí)間(1小時(shí),根據(jù)業(yè)務(wù)調(diào)整)
      cache-null-values: false # 不緩存null值,避免緩存穿透
    caffeine:
      time-to-live: 3600000 # 單機(jī)緩存過期時(shí)間
      initial-capacity: 100 # 初始緩存容量
      maximum-size: 1000 # 最大緩存數(shù)量(避免內(nèi)存溢出)
# 開啟Spring Cache注解支持
spring.cache.type: redis

策略1:Cache-Aside(旁路緩存)

Cache-Aside 是最主流、最易落地的雙寫策略,核心邏輯:查詢走緩存,更新走數(shù)據(jù)庫(kù)+刪除緩存,不直接更新緩存,避免雙寫順序錯(cuò)誤。

很多人也稱其為“Cache-Aside Pattern”,是企業(yè)開發(fā)中最常用的緩存策略,兼顧性能和一致性,實(shí)現(xiàn)簡(jiǎn)單。

1. 核心流程

查詢操作:先查緩存 → 緩存有數(shù)據(jù),直接返回;緩存無數(shù)據(jù),查數(shù)據(jù)庫(kù) → 將數(shù)據(jù)庫(kù)數(shù)據(jù)寫入緩存 → 返回?cái)?shù)據(jù);

更新操作:先更新數(shù)據(jù)庫(kù) → 再刪除緩存(而非更新緩存);

刪除操作:先刪除數(shù)據(jù)庫(kù) → 再刪除緩存。

2. 為什么是“刪除緩存”,而非“更新緩存”?

這是很多同學(xué)最常問的問題,核心原因有2點(diǎn):

  • 避免雙寫順序錯(cuò)誤:如果先更新緩存、再更新數(shù)據(jù)庫(kù),數(shù)據(jù)庫(kù)更新失敗,緩存是新數(shù)據(jù)、數(shù)據(jù)庫(kù)是舊數(shù)據(jù),直接不一致;
  •  減少冗余操作:如果多條更新操作連續(xù)執(zhí)行,每次都更新緩存,會(huì)造成不必要的性能開銷;而刪除緩存,只需在最后一次更新后刪除一次,后續(xù)查詢?cè)僦匦聦懭刖彺妫咝А?/li>

3. 完整代碼

使用Spring Cache的@Cacheable(查詢緩存)、@CacheEvict(刪除緩存)注解,無需手動(dòng)操作Redis,簡(jiǎn)化開發(fā)。

import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Optional;
/**
 * 商品服務(wù)(Cache-Aside策略實(shí)現(xiàn))
 */
@Service
public class ProductService {
    @Resource
    private ProductMapper productMapper;
    /**
     * 查詢商品:先查緩存,無則查數(shù)據(jù)庫(kù),再寫入緩存
     * value:緩存名稱(自定義)
     * key:緩存key(用商品ID,確保唯一)
     */
    @Cacheable(value = "product", key = "#id")
    public Product getProductById(Long id) {
        // 緩存沒有時(shí),查詢數(shù)據(jù)庫(kù)(實(shí)際項(xiàng)目可加日志)
        Optional<Product> product = productMapper.selectById(id);
        return product.orElse(null);
    }
    /**
     * 更新商品:先更新數(shù)據(jù)庫(kù),再刪除緩存
     * @CacheEvict:刪除緩存,allEntries=false表示只刪除當(dāng)前key的緩存
     */
    @CacheEvict(value = "product", key = "#product.id")
    public void updateProduct(Product product) {
        // 1. 先更新數(shù)據(jù)庫(kù)
        productMapper.updateById(product);
        // 2. 注解自動(dòng)刪除緩存(無需手動(dòng)操作Redis)
    }
    /**
     * 刪除商品:先刪除數(shù)據(jù)庫(kù),再刪除緩存
     */
    @CacheEvict(value = "product", key = "#id")
    public void deleteProduct(Long id) {
        // 1. 先刪除數(shù)據(jù)庫(kù)
        productMapper.deleteById(id);
        // 2. 注解自動(dòng)刪除緩存
    }
}

4. 優(yōu)缺點(diǎn)與適用場(chǎng)景

優(yōu)點(diǎn):實(shí)現(xiàn)簡(jiǎn)單、無侵入(依賴Spring Cache注解)、性能好(查詢走緩存,更新僅多一次刪除緩存操作)、一致性有保障(最終一致性);

缺點(diǎn):存在輕微的并發(fā)競(jìng)態(tài)問題(下文會(huì)講解決方案);

適用場(chǎng)景:絕大多數(shù)業(yè)務(wù)場(chǎng)景,尤其是查詢頻率高、更新頻率中等的場(chǎng)景(比如商品詳情、用戶信息、訂單列表),是企業(yè)級(jí)落地的首選。

策略2:Write-Through

Write-Through 策略的核心邏輯:更新操作時(shí),先更新數(shù)據(jù)庫(kù),再同步更新緩存;查詢操作和Cache-Aside一致(先查緩存,無則查數(shù)據(jù)庫(kù))。

這種策略的特點(diǎn)是“寫入即同步”,緩存和數(shù)據(jù)庫(kù)的數(shù)據(jù)幾乎是一致的(接近絕對(duì)一致性),但性能稍弱(多一次緩存更新操作)。

1. 核心流程

  • 查詢操作:和Cache-Aside一致(先緩存 → 再數(shù)據(jù)庫(kù) → 寫緩存);
  • 更新操作:先更新數(shù)據(jù)庫(kù) → 再更新緩存(覆蓋舊緩存);
  •  刪除操作:先刪除數(shù)據(jù)庫(kù) → 再刪除緩存(和Cache-Aside一致)。

2. 完整代碼

Write-Through 不適合用Spring Cache注解(注解無法實(shí)現(xiàn)“更新數(shù)據(jù)庫(kù)后同步更新緩存”的邏輯),需手動(dòng)操作RedisTemplate。

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@Service
public class ProductService {
    @Resource
    private ProductMapper productMapper;
    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    // 緩存key前綴(避免key沖突)
    private static final String CACHE_KEY_PREFIX = "product:";
    /**
     * 查詢商品(和Cache-Aside一致)
     */
    public Product getProductById(Long id) {
        String cacheKey = CACHE_KEY_PREFIX + id;
        // 1. 先查緩存
        Product product = (Product) redisTemplate.opsForValue().get(cacheKey);
        if (product != null) {
            return product;
        }
        // 2. 緩存無,查數(shù)據(jù)庫(kù)
        Optional<Product> dbProduct = productMapper.selectById(id);
        if (dbProduct.isPresent()) {
            // 3. 寫入緩存(設(shè)置過期時(shí)間,避免緩存雪崩)
            redisTemplate.opsForValue().set(cacheKey, dbProduct.get(), 1, TimeUnit.HOURS);
            return dbProduct.get();
        }
        return null;
    }
    /**
     * 更新商品:先更數(shù)據(jù)庫(kù),再更緩存(Write-Through策略核心)
     */
    public void updateProduct(Product product) {
        // 1. 先更新數(shù)據(jù)庫(kù)
        productMapper.updateById(product);
        // 2. 同步更新緩存(覆蓋舊數(shù)據(jù))
        String cacheKey = CACHE_KEY_PREFIX + product.getId();
        redisTemplate.opsForValue().set(cacheKey, product, 1, TimeUnit.HOURS);
    }
    /**
     * 刪除商品:先刪數(shù)據(jù)庫(kù),再刪緩存
     */
    public void deleteProduct(Long id) {
        // 1. 先刪除數(shù)據(jù)庫(kù)
        productMapper.deleteById(id);
        // 2. 再刪除緩存
        String cacheKey = CACHE_KEY_PREFIX + id;
        redisTemplate.delete(cacheKey);
    }
}

3. 優(yōu)缺點(diǎn)與適用場(chǎng)景

優(yōu)點(diǎn):緩存與數(shù)據(jù)庫(kù)一致性強(qiáng)(接近絕對(duì)一致),查詢時(shí)不會(huì)出現(xiàn)舊數(shù)據(jù),適合對(duì)數(shù)據(jù)一致性要求高的場(chǎng)景;

缺點(diǎn):性能稍弱(更新操作多一次緩存寫入),存在雙寫順序錯(cuò)誤風(fēng)險(xiǎn)(若更新緩存失敗,數(shù)據(jù)庫(kù)是新數(shù)據(jù)、緩存是舊數(shù)據(jù));

適用場(chǎng)景:對(duì)數(shù)據(jù)一致性要求高、更新頻率低的場(chǎng)景(比如金融數(shù)據(jù)、核心配置數(shù)據(jù)),不適合高頻更新場(chǎng)景。

策略3:Write-Back(寫回)

Write-Back 策略的核心邏輯:更新操作時(shí),先更新緩存,不立即更新數(shù)據(jù)庫(kù),而是將緩存標(biāo)記為“臟數(shù)據(jù)”,在一定時(shí)機(jī)(比如緩存過期、緩存滿了、定時(shí)任務(wù))再批量同步到數(shù)據(jù)庫(kù)。

這種策略的特點(diǎn)是“寫入性能極高”(只需更新緩存,無需立即操作數(shù)據(jù)庫(kù)),但一致性最弱(緩存更新后,數(shù)據(jù)庫(kù)可能還是舊數(shù)據(jù)),實(shí)現(xiàn)復(fù)雜,很少在業(yè)務(wù)系統(tǒng)中使用。

1. 核心流程

  • 查詢操作:和前兩種策略一致(先緩存 → 再數(shù)據(jù)庫(kù) → 寫緩存);
  • 更新操作:先更新緩存 → 標(biāo)記緩存為“臟數(shù)據(jù)” → 異步/定時(shí)同步到數(shù)據(jù)庫(kù);
  • 刪除操作:先刪除緩存 → 標(biāo)記為“臟數(shù)據(jù)” → 異步/定時(shí)刪除數(shù)據(jù)庫(kù)數(shù)據(jù)。

2. 簡(jiǎn)化實(shí)現(xiàn)代碼

Write-Back 實(shí)現(xiàn)復(fù)雜,需結(jié)合定時(shí)任務(wù)、臟數(shù)據(jù)標(biāo)記,以下是簡(jiǎn)化版核心邏輯(實(shí)際落地需完善異常處理、重試機(jī)制):

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@Service
public class ProductService {
    @Resource
    private ProductMapper productMapper;
    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    private static final String CACHE_KEY_PREFIX = "product:";
    // 存儲(chǔ)臟數(shù)據(jù)(key:緩存key,value:商品對(duì)象)
    private final Map<String, Product> dirtyDataMap = new HashMap<>();
    /**
     * 查詢商品
     */
    public Product getProductById(Long id) {
        String cacheKey = CACHE_KEY_PREFIX + id;
        Product product = (Product) redisTemplate.opsForValue().get(cacheKey);
        if (product != null) {
            return product;
        }
        Optional<Product> dbProduct = productMapper.selectById(id);
        if (dbProduct.isPresent()) {
            redisTemplate.opsForValue().set(cacheKey, dbProduct.get(), 1, TimeUnit.HOURS);
            return dbProduct.get();
        }
        return null;
    }
    /**
     * 更新商品:先更緩存,標(biāo)記臟數(shù)據(jù)(Write-Back核心)
     */
    public void updateProduct(Product product) {
        String cacheKey = CACHE_KEY_PREFIX + product.getId();
        // 1. 更新緩存
        redisTemplate.opsForValue().set(cacheKey, product, 1, TimeUnit.HOURS);
        // 2. 標(biāo)記為臟數(shù)據(jù)
        dirtyDataMap.put(cacheKey, product);
    }
    /**
     * 定時(shí)同步臟數(shù)據(jù)到數(shù)據(jù)庫(kù)(每5分鐘執(zhí)行一次,可調(diào)整)
     */
    @Scheduled(cron = "0 0/5 * * * ?")
    public void syncDirtyDataToDb() {
        if (dirtyDataMap.isEmpty()) {
            return;
        }
        // 批量同步臟數(shù)據(jù)到數(shù)據(jù)庫(kù)
        for (Product product : dirtyDataMap.values()) {
            productMapper.updateById(product);
        }
        // 清空臟數(shù)據(jù)
        dirtyDataMap.clear();
    }
}

3. 優(yōu)缺點(diǎn)與適用場(chǎng)景

優(yōu)點(diǎn):寫入性能極高(無需立即操作數(shù)據(jù)庫(kù)),適合高頻寫入、對(duì)一致性要求低的場(chǎng)景;

缺點(diǎn):一致性最弱(緩存更新后,數(shù)據(jù)庫(kù)可能延遲同步,若系統(tǒng)崩潰,臟數(shù)據(jù)會(huì)丟失),實(shí)現(xiàn)復(fù)雜(需處理臟數(shù)據(jù)、定時(shí)同步、異常重試);

適用場(chǎng)景:高頻寫入、對(duì)數(shù)據(jù)一致性要求低的場(chǎng)景(比如日志緩存、瀏覽記錄、臨時(shí)統(tǒng)計(jì)數(shù)據(jù)),業(yè)務(wù)系統(tǒng)核心數(shù)據(jù)不推薦使用。

三、解決雙寫策略的并發(fā)競(jìng)態(tài)問題

前面提到,Cache-Aside 策略存在輕微的并發(fā)競(jìng)態(tài)問題,這是新手落地時(shí)最容易踩的坑,也是面試常問的點(diǎn),下面拆解問題場(chǎng)景,并給出兩種企業(yè)級(jí)解決方案。

1. 并發(fā)競(jìng)態(tài)問題場(chǎng)景

假設(shè)兩個(gè)線程同時(shí)執(zhí)行:線程A(更新操作)、線程B(查詢操作),執(zhí)行順序如下:

1. 線程A:更新數(shù)據(jù)庫(kù)(成功);

2. 線程A:準(zhǔn)備刪除緩存(還未執(zhí)行);

3. 線程B:查詢緩存(緩存中還有舊數(shù)據(jù)?不,此時(shí)緩存還未刪除,線程B查到舊數(shù)據(jù),準(zhǔn)備返回);

4. 線程A:刪除緩存(成功);

5. 線程B:將查到的舊數(shù)據(jù),重新寫入緩存;

最終結(jié)果:數(shù)據(jù)庫(kù)是新數(shù)據(jù),緩存是舊數(shù)據(jù),出現(xiàn)一致性問題,且后續(xù)查詢都會(huì)拿到舊數(shù)據(jù)(直到緩存過期)。

2. 解決方案1:延遲刪除緩存

核心邏輯:更新數(shù)據(jù)庫(kù)后,延遲一段時(shí)間(比如100毫秒)再刪除緩存,確保線程B在查詢時(shí),能查到數(shù)據(jù)庫(kù)的新數(shù)據(jù),而不是舊數(shù)據(jù)后寫入緩存。

實(shí)現(xiàn)方式:使用線程池異步延遲刪除,不影響主線程性能。

import org.springframework.cache.annotation.Cacheable;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@Service
public class ProductService {
    @Resource
    private ProductMapper productMapper;
    @Resource
    private ThreadPoolTaskExecutor taskExecutor;
    /**
     * 查詢商品(不變)
     */
    @Cacheable(value = "product", key = "#id")
    public Product getProductById(Long id) {
        Optional<Product> product = productMapper.selectById(id);
        return product.orElse(null);
    }
    /**
     * 更新商品:延遲刪除緩存,解決并發(fā)競(jìng)態(tài)
     */
    public void updateProduct(Product product) {
        // 1. 先更新數(shù)據(jù)庫(kù)
        productMapper.updateById(product);
        // 2. 異步延遲100毫秒刪除緩存(延遲時(shí)間可調(diào)整)
        Long productId = product.getId();
        taskExecutor.schedule(() -> {
            // 手動(dòng)刪除緩存(替代@CacheEvict注解)
            redisTemplate.delete("product:" + productId);
        }, 100, TimeUnit.MILLISECONDS);
    }
}

? 關(guān)鍵說明:延遲時(shí)間建議設(shè)置為“業(yè)務(wù)接口的最大響應(yīng)時(shí)間”(比如100-500毫秒),確保線程B的查詢操作能在緩存刪除前完成數(shù)據(jù)庫(kù)查詢,避免舊數(shù)據(jù)寫入緩存。

3. 解決方案2:分布式鎖

核心邏輯:在查詢和更新操作中,給“緩存key”加分布式鎖(比如Redis分布式鎖),確保同一時(shí)間,只有一個(gè)線程能執(zhí)行“查詢+寫緩存”或“更新+刪緩存”操作,徹底解決競(jìng)態(tài)問題。

實(shí)現(xiàn)方式:使用Redisson分布式鎖(簡(jiǎn)化鎖的操作,避免死鎖),適合分布式系統(tǒng)場(chǎng)景。

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@Service
public class ProductService {
    @Resource
    private ProductMapper productMapper;
    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    @Resource
    private RedissonClient redissonClient;
    private static final String CACHE_KEY_PREFIX = "product:";
    private static final String LOCK_KEY_PREFIX = "product:lock:";
    /**
     * 查詢商品:加分布式鎖,避免競(jìng)態(tài)
     */
    public Product getProductById(Long id) {
        String cacheKey = CACHE_KEY_PREFIX + id;
        String lockKey = LOCK_KEY_PREFIX + id;
        RLock lock = redissonClient.getLock(lockKey);
        try {
            // 加鎖(10秒自動(dòng)釋放,避免死鎖)
            lock.lock(10, TimeUnit.SECONDS);
            // 1. 先查緩存
            Product product = (Product) redisTemplate.opsForValue().get(cacheKey);
            if (product != null) {
                return product;
            }
            // 2. 查數(shù)據(jù)庫(kù),寫緩存
            Optional<Product> dbProduct = productMapper.selectById(id);
            if (dbProduct.isPresent()) {
                redisTemplate.opsForValue().set(cacheKey, dbProduct.get(), 1, TimeUnit.HOURS);
                return dbProduct.get();
            }
            return null;
        } finally {
            // 釋放鎖
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
    /**
     * 更新商品:加分布式鎖,避免競(jìng)態(tài)
     */
    public void updateProduct(Product product) {
        String cacheKey = CACHE_KEY_PREFIX + product.getId();
        String lockKey = LOCK_KEY_PREFIX + product.getId();
        RLock lock = redissonClient.getLock(lockKey);
        try {
            lock.lock(10, TimeUnit.SECONDS);
            // 1. 更新數(shù)據(jù)庫(kù)
            productMapper.updateById(product);
            // 2. 刪除緩存
            redisTemplate.delete(cacheKey);
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

? 關(guān)鍵說明:分布式鎖會(huì)增加一定的性能開銷,適合對(duì)一致性要求高的分布式系統(tǒng);如果是單機(jī)系統(tǒng),可用本地鎖(synchronized)替代,更高效。

四、文末小結(jié)

重點(diǎn):優(yōu)先掌握 Cache-Aside 策略(最易落地、最常用),先實(shí)現(xiàn)“查詢查緩存、更新刪緩存”的基礎(chǔ)邏輯,再添加延遲刪除緩存解決競(jìng)態(tài)問題,配合緩存過期時(shí)間、異常重試,就能滿足絕大多數(shù)業(yè)務(wù)場(chǎng)景的緩存一致性需求。

實(shí)際項(xiàng)目中,無需過度追求復(fù)雜的策略,根據(jù)業(yè)務(wù)場(chǎng)景選擇合適的雙寫方案:查詢高頻、更新中等 → Cache-Aside;一致性要求高 → Write-Through;高頻寫入、一致性要求低 → Write-Back。

以上就是SpringBoot實(shí)現(xiàn)緩存與數(shù)據(jù)庫(kù)雙寫策略的詳細(xì)代碼的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot緩存與數(shù)據(jù)庫(kù)雙寫策略的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • JDBC Template基本使用方法詳解

    JDBC Template基本使用方法詳解

    這篇文章主要介紹了JDBC Template基本使用方法詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-06-06
  • Java中l(wèi)ist.foreach()和list.stream().foreach()用法詳解

    Java中l(wèi)ist.foreach()和list.stream().foreach()用法詳解

    在Java中List是一種常用的集合類,用于存儲(chǔ)一組元素,List提供了多種遍歷元素的方式,包括使用forEach()方法和使用Stream流的forEach()方法,這篇文章主要給大家介紹了關(guān)于Java中l(wèi)ist.foreach()和list.stream().foreach()用法的相關(guān)資料,需要的朋友可以參考下
    2024-07-07
  • 基于Java編寫一個(gè)html轉(zhuǎn)pdf的工具類

    基于Java編寫一個(gè)html轉(zhuǎn)pdf的工具類

    這篇文章主要為大家詳細(xì)介紹了如何基于Java編寫一個(gè)html轉(zhuǎn)pdf的工具類,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起了解下
    2025-09-09
  • 關(guān)于@SpringBootApplication詳解

    關(guān)于@SpringBootApplication詳解

    這篇文章主要介紹了關(guān)于@SpringBootApplication的使用方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2024-08-08
  • SpringBoot應(yīng)用整合ELK實(shí)現(xiàn)日志收集的示例代碼

    SpringBoot應(yīng)用整合ELK實(shí)現(xiàn)日志收集的示例代碼

    這篇文章主要介紹了SpringBoot應(yīng)用整合ELK實(shí)現(xiàn)日志收集的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-09-09
  • Java線程池框架核心代碼解析

    Java線程池框架核心代碼解析

    這篇文章主要針對(duì)Java線程池框架核心代碼進(jìn)行詳細(xì)解析,分析Java線程池框架的實(shí)現(xiàn)ThreadPoolExecutor,感興趣的小伙伴們可以參考一下
    2016-07-07
  • MyBatis 動(dòng)態(tài) SQL 優(yōu)化之標(biāo)簽的實(shí)戰(zhàn)與技巧(常見用法)

    MyBatis 動(dòng)態(tài) SQL 優(yōu)化之標(biāo)簽的實(shí)戰(zhàn)與技巧(常見用法)

    本文通過詳細(xì)的示例和實(shí)際應(yīng)用場(chǎng)景,介紹了如何有效利用這些標(biāo)簽來優(yōu)化 MyBatis 配置,提升開發(fā)效率,確保 SQL 的高效執(zhí)行和安全性,感興趣的朋友跟隨小編一起看看吧
    2025-04-04
  • Java中的封裝、繼承和多態(tài),你真的都懂了嗎

    Java中的封裝、繼承和多態(tài),你真的都懂了嗎

    Java中的封裝、繼承和多態(tài)知識(shí)點(diǎn)是學(xué)習(xí)java必備的基礎(chǔ)知識(shí),看似簡(jiǎn)單,真正理解起來還是有一定難度的,今天小編再次通過實(shí)例代碼給大家講解java 封裝繼承多態(tài)知識(shí),感興趣的朋友一起學(xué)習(xí)下吧
    2021-05-05
  • 淺談Java之Map 按值排序 (Map sort by value)

    淺談Java之Map 按值排序 (Map sort by value)

    下面小編就為大家?guī)硪黄獪\談Java之Map 按值排序 (Map sort by value)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2016-08-08
  • Docker使用 Maven 插件構(gòu)建鏡像的方法

    Docker使用 Maven 插件構(gòu)建鏡像的方法

    本篇文章主要介紹了Docker使用 Maven 插件構(gòu)建鏡像的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2018-02-02

最新評(píng)論

沁水县| 炉霍县| 墨玉县| 湖南省| 沙田区| 怀柔区| 香港 | 遂溪县| 城市| 黑山县| 西乌珠穆沁旗| 留坝县| 民乐县| 绵阳市| 苍溪县| 筠连县| 潜江市| 百色市| 讷河市| 焉耆| 长武县| 旬邑县| 高青县| 甘南县| 册亨县| 黑龙江省| 嘉定区| 清涧县| 龙南县| 林芝县| 大悟县| 灵武市| 金乡县| 邮箱| 遂川县| 赫章县| 扎鲁特旗| 岗巴县| 呼和浩特市| 千阳县| 扎赉特旗|