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

Redis雙重判定鎖的實現(xiàn)(緩存擊穿的終極解決方案)

 更新時間:2026年05月24日 15:10:05   作者:山沐與山  
本文詳細介紹了雙重判定鎖在分布式系統(tǒng)的應(yīng)用,通過兩次檢查避免了不必要的數(shù)據(jù)庫查詢,適用于緩存擊穿場景,提升系統(tǒng)效率與性能,感興趣的可以了解一下

前言

這篇是微服務(wù)全家桶系列的學(xué)習(xí)筆記,這次整理的是分布式場景下的雙重判定鎖(Double-Checked Locking,簡稱 DCL)。

最近在做短鏈接跳轉(zhuǎn)這塊業(yè)務(wù),遇到了一個挺有意思的問題:緩存里沒數(shù)據(jù)的時候,一堆請求同時涌進來,全都去查數(shù)據(jù)庫,數(shù)據(jù)庫直接被打趴了。你想想,一個熱點短鏈接每秒幾萬次訪問,緩存一過期,這幾萬個請求全部打到 MySQL,這誰頂?shù)米。?/p>

后來引入了分布式鎖,但又發(fā)現(xiàn)一個問題:鎖是加上了,可第一個請求把數(shù)據(jù)寫回緩存后,后面排隊的請求拿到鎖還是傻乎乎地去查數(shù)據(jù)庫。這不是多此一舉嗎?

這就是雙重判定鎖要解決的問題——鎖外檢查一次,鎖內(nèi)再檢查一次,既保證了并發(fā)安全,又避免了無謂的數(shù)據(jù)庫查詢。

一、緩存的三大經(jīng)典問題

在聊雙重判定鎖之前,得先搞清楚為什么需要它。緩存在分布式系統(tǒng)里基本是標(biāo)配了,但用不好就會出問題。

1.1 緩存穿透

什么情況? 用戶老是查一個根本不存在的數(shù)據(jù),每次都打到數(shù)據(jù)庫。

比如有人惡意請求 id=-1 的數(shù)據(jù),數(shù)據(jù)庫里根本沒有,緩存自然也存不了,每次請求都穿透到數(shù)據(jù)庫。

怎么解決?

  • 布隆過濾器:先判斷數(shù)據(jù)可能不可能存在
  • 空值緩存:查不到也緩存?zhèn)€占位符,下次直接返回

1.2 緩存擊穿

什么情況? 熱點 Key 突然過期,大量請求同時打到數(shù)據(jù)庫。

這是本文的重點。假設(shè)有個爆款短鏈接,每秒 10 萬次訪問,緩存過期的那一瞬間,10 萬個請求全部去查數(shù)據(jù)庫。這不是擊穿是什么?

怎么解決?

  • 分布式鎖:只讓一個請求去查庫,其他人等著
  • 雙重判定鎖:拿到鎖后再檢查一次,避免重復(fù)查庫

1.3 緩存雪崩

什么情況? 大量 Key 同時過期,數(shù)據(jù)庫壓力驟增。

怎么解決?

  • 隨機過期時間:別讓大家同時過期
  • 永不過期策略:后臺異步更新

1.4 三者對比

問題觸發(fā)條件危害解決方案
緩存穿透查詢不存在的數(shù)據(jù)數(shù)據(jù)庫被無效請求打滿布隆過濾器 + 空值緩存
緩存擊穿熱點 Key 過期瞬時高并發(fā)打到數(shù)據(jù)庫分布式鎖 + 雙重判定
緩存雪崩大量 Key 同時過期數(shù)據(jù)庫持續(xù)高壓隨機過期時間

二、什么是雙重判定鎖

2.1 核心思想

雙重判定鎖的核心就三步:

  1. 第一次檢查(鎖外):先看緩存有沒有,有就直接返回,不用加鎖
  2. 獲取鎖:緩存沒有才去搶鎖
  3. 第二次檢查(鎖內(nèi)):拿到鎖后再看一眼緩存,因為等鎖的時候別人可能已經(jīng)把數(shù)據(jù)放進去了

為什么要檢查兩次?舉個例子就明白了。

2.2 一個生動的例子

假設(shè)食堂打飯,窗口只有一個阿姨(數(shù)據(jù)庫),學(xué)生們排隊(請求)。

沒有雙重判定

學(xué)生A看到菜沒了 → 叫阿姨去廚房拿
學(xué)生B看到菜沒了 → 也叫阿姨去廚房拿
學(xué)生C看到菜沒了 → 也叫阿姨去廚房拿
... (阿姨被叫煩了)

阿姨跑了一趟拿回來菜,結(jié)果后面幾個學(xué)生還在叫她去拿,因為他們不知道已經(jīng)有人拿回來了。

有雙重判定

學(xué)生A看到菜沒了 → 舉手說"我去找阿姨"
學(xué)生B看到菜沒了 → 發(fā)現(xiàn)有人舉手了,等著
學(xué)生C看到菜沒了 → 發(fā)現(xiàn)有人舉手了,等著
學(xué)生A叫完阿姨,菜回來了
學(xué)生B看了一眼,哦有菜了,直接打
學(xué)生C看了一眼,哦有菜了,直接打

關(guān)鍵點:學(xué)生 BC 等到可以行動的時候,先看一眼有沒有菜,而不是直接去叫阿姨。這就是雙重判定——拿到行動權(quán)后再確認一次

2.3 偽代碼表示

public String getData(String key) {
    // [第一次檢查] 鎖外檢查緩存
    String value = cache.get(key);
    if (value != null) {
        return value;  // 緩存命中,直接返回
    }
    // 緩存未命中,獲取分布式鎖
    RLock lock = redissonClient.getLock("lock:" + key);
    lock.lock();
    try {
        // [第二次檢查] 鎖內(nèi)再檢查一次!
        value = cache.get(key);
        if (value != null) {
            return value;  // 其他線程已經(jīng)加載了,直接用
        }
        // 確實沒有,去查數(shù)據(jù)庫
        value = db.query(key);
        cache.set(key, value);
        return value;
    } finally {
        lock.unlock();
    }
}

看到?jīng)]?lock.lock() 之后的第一件事不是查數(shù)據(jù)庫,而是再檢查一次緩存。因為在你等鎖的這段時間里,拿到鎖的那個線程可能已經(jīng)把數(shù)據(jù)放到緩存里了。

三、實戰(zhàn)代碼解析

來看一段真實項目中的代碼,這是短鏈接跳轉(zhuǎn)服務(wù)的核心邏輯。

3.1 Redis Key 設(shè)計

public class RedisKeyConstant {

    // 短鏈接跳轉(zhuǎn)緩存:fullShortUrl -> originUrl
    public static final String GOTO_SHORT_LINK_KEY = "short-link:goto:%s";

    // 空值緩存:標(biāo)記不存在的短鏈接
    public static final String GOTO_IS_NULL_SHORT_LINK_KEY = "short-link:is-null:goto_%s";

    // 分布式鎖:防止緩存擊穿
    public static final String LOCK_GOTO_SHORT_LINK_KEY = "short-link:lock:goto:%s";
}

這里設(shè)計了三個 Key

  • GOTO_SHORT_LINK_KEY:正常的跳轉(zhuǎn)緩存
  • GOTO_IS_NULL_SHORT_LINK_KEY:空值緩存,防止緩存穿透
  • LOCK_GOTO_SHORT_LINK_KEY:分布式鎖的 Key

3.2 核心跳轉(zhuǎn)邏輯

@SneakyThrows
@Override
public void restoreUrl(String shortUri, ServletRequest request, ServletResponse response) {
    // 構(gòu)建完整短鏈接
    String serverName = request.getServerName();
    String serverPort = Optional.of(request.getServerPort())
            .filter(each -> !Objects.equals(each, 80))
            .map(String::valueOf)
            .map(each -> ":" + each)
            .orElse("");
    String fullShortUrl = serverName + serverPort + "/" + shortUri;

    // ==================== 第一次判斷(鎖外)====================

    // [檢查點1] 查緩存
    String originLink = stringRedisTemplate.opsForValue()
            .get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
    if (StrUtil.isNotBlank(originLink)) {
        // 緩存命中,記錄統(tǒng)計后直接跳轉(zhuǎn)
        shortLinkStats(fullShortUrl, null, buildStatsRecord(fullShortUrl, request, response));
        ((HttpServletResponse) response).sendRedirect(originLink);
        return;
    }

    // [檢查點2] 布隆過濾器判斷
    boolean contains = shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl);
    if (!contains) {
        // 布隆過濾器說不存在,那就一定不存在
        ((HttpServletResponse) response).sendRedirect("/page/notfound");
        return;
    }

    // [檢查點3] 檢查空值緩存
    String gotoIsNullShortLink = stringRedisTemplate.opsForValue()
            .get(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl));
    if (StrUtil.isNotBlank(gotoIsNullShortLink)) {
        // 已確認不存在的短鏈接
        ((HttpServletResponse) response).sendRedirect("/page/notfound");
        return;
    }

    // ==================== 獲取分布式鎖 ====================
    RLock lock = redissonClient.getLock(String.format(LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl));
    lock.lock();

    try {
        // ==================== 第二次判斷(鎖內(nèi))====================

        // [雙重檢查1] 再查一次緩存
        originLink = stringRedisTemplate.opsForValue()
                .get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
        if (StrUtil.isNotBlank(originLink)) {
            // 其他線程已加載緩存,直接使用
            shortLinkStats(fullShortUrl, null, buildStatsRecord(fullShortUrl, request, response));
            ((HttpServletResponse) response).sendRedirect(originLink);
            return;
        }

        // [雙重檢查2] 再查一次空值緩存
        gotoIsNullShortLink = stringRedisTemplate.opsForValue()
                .get(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl));
        if (StrUtil.isNotBlank(gotoIsNullShortLink)) {
            ((HttpServletResponse) response).sendRedirect("/page/notfound");
            return;
        }

        // ==================== 查詢數(shù)據(jù)庫 ====================

        // 先查路由表拿 gid(因為主表是按 gid 分表的)
        LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
        ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(linkGotoQueryWrapper);

        if (shortLinkGotoDO == null) {
            // 路由表沒有,設(shè)置空值緩存
            stringRedisTemplate.opsForValue()
                    .set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES);
            ((HttpServletResponse) response).sendRedirect("/page/notfound");
            return;
        }

        // 查短鏈接詳情
        LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid())
                .eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
                .eq(ShortLinkDO::getEnableStatus, 0)
                .eq(ShortLinkDO::getDelFlag, 0);
        ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper);

        // 檢查是否存在或過期
        if (shortLinkDO == null || (shortLinkDO.getValidDate() != null
                && shortLinkDO.getValidDate().before(new Date()))) {
            stringRedisTemplate.opsForValue()
                    .set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES);
            ((HttpServletResponse) response).sendRedirect("/page/notfound");
            return;
        }

        // ==================== 寫入緩存并跳轉(zhuǎn) ====================
        stringRedisTemplate.opsForValue()
                .set(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
                     shortLinkDO.getOriginUrl(),
                     LinkUtil.getLinkCacheValidTime(shortLinkDO.getValidDate()),
                     TimeUnit.MILLISECONDS);

        shortLinkStats(fullShortUrl, shortLinkDO.getGid(), buildStatsRecord(fullShortUrl, request, response));
        ((HttpServletResponse) response).sendRedirect(shortLinkDO.getOriginUrl());

    } finally {
        lock.unlock();
    }
}

3.3 代碼分層解讀

這段代碼分成四層,層層遞進:

層級位置檢查內(nèi)容作用
第一層鎖外緩存命中直接返回,不加鎖
第二層鎖外布隆過濾器快速拒絕不存在的請求
第三層鎖外空值緩存攔截已確認不存在的短鏈接
第四層鎖內(nèi)雙重判定避免等鎖期間的重復(fù)查庫

為什么要這么多層?因為越早返回越好。能在鎖外解決的事情,就不要進鎖;能在緩存解決的事情,就不要查數(shù)據(jù)庫。

四、完整流程圖解

4.1 請求處理流程

                               用戶請求
                                  │
                                  ▼
                      ┌───────────────────────┐
                      │   構(gòu)建 fullShortUrl    │
                      └───────────────────────┘
                                  │
        ┌─────────────────────────┴─────────────────────────┐
        │                     無鎖區(qū)域                        │
        │  ┌─────────────┐     命中     ┌──────────────┐    │
        │  │  查緩存      │────────────?│  直接跳轉(zhuǎn)     │    │
        │  └─────────────┘              └──────────────┘    │
        │        │ 未命中                                    │
        │        ▼                                          │
        │  ┌─────────────┐    不存在    ┌──────────────┐    │
        │  │  布隆過濾器  │────────────?│  返回 404    │    │
        │  └─────────────┘              └──────────────┘    │
        │        │ 可能存在                                  │
        │        ▼                                          │
        │  ┌─────────────┐     存在     ┌──────────────┐    │
        │  │  空值緩存    │────────────?│  返回 404    │    │
        │  └─────────────┘              └──────────────┘    │
        │        │ 不存在                                    │
        └────────┴──────────────────────────────────────────┘
                 │
                 ▼
        ┌───────────────────────┐
        │     獲取分布式鎖       │
        │     lock.lock()       │
        └───────────────────────┘
                 │
        ┌────────┴──────────────────────────────────────────┐
        │                     有鎖區(qū)域                        │
        │  ┌─────────────┐     命中     ┌──────────────┐    │
        │  │  再查緩存    │────────────?│  直接跳轉(zhuǎn)     │    │
        │  │  (雙重判定)  │              │  (別人加載的) │    │
        │  └─────────────┘              └──────────────┘    │
        │        │ 仍未命中                                  │
        │        ▼                                          │
        │  ┌─────────────┐     存在     ┌──────────────┐    │
        │  │  再查空值    │────────────?│  返回 404    │    │
        │  │  (雙重判定)  │              └──────────────┘    │
        │  └─────────────┘                                  │
        │        │ 仍不存在                                  │
        │        ▼                                          │
        │  ┌─────────────────────────────────────────┐      │
        │  │              查詢數(shù)據(jù)庫                   │      │
        │  │   路由表 → 短鏈接表 → 寫入緩存 → 跳轉(zhuǎn)     │      │
        │  └─────────────────────────────────────────┘      │
        └───────────────────────────────────────────────────┘
                 │
                 ▼
        ┌───────────────────────┐
        │      釋放鎖            │
        │      lock.unlock()    │
        └───────────────────────┘

4.2 并發(fā)場景時序圖

假設(shè)三個請求幾乎同時到來,緩存為空:

時間軸 ──────────────────────────────────────────────────────?
請求A ─────┬──────────────────────────────────────────────────
           │  查緩存 → 未命中
           │  查布隆 → 可能存在
           │  查空值 → 不存在
           │  獲取鎖 ?
           │  雙重判定 → 仍未命中
           │  查數(shù)據(jù)庫...
           │  寫入緩存 ?──────────────── 這時候緩存有值了
           │  釋放鎖
           └──? 跳轉(zhuǎn)成功
請求B ───────────┬────────────────────────────────────────────
                 │  查緩存 → 未命中
                 │  查布隆 → 可能存在
                 │  查空值 → 不存在
                 │  等待鎖... ?
                 │      │
                 │      ▼ (A釋放鎖后)
                 │  獲取鎖 ?
                 │  雙重判定 → 命中!(A已寫入)
                 │  釋放鎖
                 └──? 直接跳轉(zhuǎn),沒查庫!
請求C ───────────────────────────────────────────────────┬────
                                                         │  查緩存 → 命中!
                                                         └──? 直接跳轉(zhuǎn),沒加鎖!

看到效果了吧?

  • 請求 A:第一個到,老老實實查庫
  • 請求 B:等到鎖后發(fā)現(xiàn)緩存已有值,直接用,不查庫
  • 請求 C:來得晚,連鎖都不用加,緩存里直接拿

五、最佳實踐與踩坑記錄

5.1 鎖粒度要細

// ? 正確:每個短鏈接一把鎖
RLock lock = redissonClient.getLock(
    String.format(LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl)
);

// ? 錯誤:全局一把鎖
RLock lock = redissonClient.getLock("short-link:global-lock");

全局鎖會導(dǎo)致所有請求串行化,性能急劇下降。正確的做法是按資源粒度加鎖,每個短鏈接有自己的鎖,互不影響。

5.2 先檢查正常緩存,再檢查空值緩存

有人可能會問:為什么拿到鎖后先查正常緩存,而不是先查空值緩存?

lock.lock();
try {
    // 先查正常緩存
    originLink = cache.get(GOTO_KEY);
    if (StrUtil.isNotBlank(originLink)) {
        return originLink;
    }

    // 再查空值緩存
    gotoIsNull = cache.get(IS_NULL_KEY);
    if (StrUtil.isNotBlank(gotoIsNull)) {
        return 404;
    }
    // ...
}

原因是:我們假設(shè)大部分請求都是正常的。如果把空值緩存檢查放前面,意味著假設(shè)系統(tǒng)經(jīng)常被攻擊。而實際情況是,正常請求遠多于惡意請求,所以優(yōu)先檢查正常緩存能減少一次無謂的 Redis 查詢。

5.3 空值緩存要設(shè)過期時間

// 設(shè)置 30 分鐘過期
stringRedisTemplate.opsForValue()
    .set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES);

為什么?假設(shè)短鏈接被誤刪后又恢復(fù)了,如果空值緩存永不過期,用戶就永遠訪問不了。30 分鐘是個平衡點——既能防止短期內(nèi)的穿透攻擊,又不會影響數(shù)據(jù)恢復(fù)后的正常訪問。

5.4 用 lock() 而不是 tryLock()

// 當(dāng)前實現(xiàn):阻塞等待
lock.lock();

// 為什么不用這個?
// if (!lock.tryLock()) {
//     throw new ServiceException("系統(tǒng)繁忙,請稍后再試");
// }

因為短鏈接跳轉(zhuǎn)是用戶的核心操作,不應(yīng)該因為鎖競爭就直接失敗。用 lock() 讓請求排隊,最終都能得到正確結(jié)果。用 tryLock() 雖然快,但用戶體驗差——憑什么我點一下就失敗了?

5.5 緩存更新時的清理策略

當(dāng)數(shù)據(jù)變更時,記得清理相關(guān)緩存:

// 移入回收站:刪除跳轉(zhuǎn)緩存
public void saveRecycleBin(RecycleBinSaveReqDTO requestParam) {
    // ... 更新數(shù)據(jù)庫
    stringRedisTemplate.delete(String.format(GOTO_SHORT_LINK_KEY, requestParam.getFullShortUrl()));
}

// 從回收站恢復(fù):刪除空值緩存
public void recoverRecycleBin(RecycleBinRecoverReqDTO requestParam) {
    // ... 更新數(shù)據(jù)庫
    stringRedisTemplate.delete(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, requestParam.getFullShortUrl()));
}

這點容易被忽略。短鏈接禁用時要刪跳轉(zhuǎn)緩存,恢復(fù)時要刪空值緩存,否則會出現(xiàn)緩存和數(shù)據(jù)庫不一致的問題。

六、常見問題

6.1 布隆過濾器說存在就一定存在嗎?

不是。布隆過濾器的特性是:

  • 說不存在 → 一定不存在(可信)
  • 說存在 → 可能存在(有誤判率)

所以即使布隆過濾器判斷存在,也還需要后續(xù)的檢查。項目里配置的誤判率是 0.001(千分之一),基本上影響不大。

// 預(yù)估 1000 萬條數(shù)據(jù),誤判率 0.001
cachePenetrationBloomFilter.tryInit(10000000, 0.001);

6.2 為什么不用讀寫鎖?

其實項目里在另一個場景用了讀寫鎖——修改短鏈接分組 gid 的時候:

// 修改 gid 時加寫鎖
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(
    String.format(LOCK_GID_UPDATE_KEY, fullShortUrl)
);
RLock wLock = readWriteLock.writeLock();
wLock.lock();

// 統(tǒng)計訪問時加讀鎖
RLock rLock = readWriteLock.readLock();
rLock.lock();

但在跳轉(zhuǎn)這個場景不適合用讀寫鎖。因為跳轉(zhuǎn)時大部分時間是"讀緩存",不需要加鎖;只有緩存未命中時才需要"寫緩存",這時候用普通鎖就夠了。

6.3 雙重判定鎖是不是萬能的?

不是。它主要解決緩存擊穿問題,對于緩存雪崩(大量 Key 同時過期)效果有限。雪崩問題需要其他手段:

問題解決方案
緩存擊穿分布式鎖 + 雙重判定 ?
緩存雪崩隨機過期時間、熱點數(shù)據(jù)永不過期
緩存穿透布隆過濾器 + 空值緩存

6.4 鎖的粒度多細合適?

一般按業(yè)務(wù) Key 來加鎖。比如短鏈接跳轉(zhuǎn)場景,就按 fullShortUrl 加鎖:

// 鎖的粒度 = 單個短鏈接
String lockKey = String.format(LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl);

粒度太粗(全局鎖)會導(dǎo)致串行化,粒度太細(比如按用戶 IP)沒有意義。原則是:不同的業(yè)務(wù)資源之間不應(yīng)該互相阻塞。

七、總結(jié)

本文介紹了分布式場景下雙重判定鎖的設(shè)計與實現(xiàn),重點包括:

  1. 緩存三大問題:穿透、擊穿、雪崩的區(qū)別與解決方案
  2. 雙重判定鎖原理:鎖外檢查一次,鎖內(nèi)再檢查一次
  3. 實戰(zhàn)代碼:短鏈接跳轉(zhuǎn)服務(wù)的完整實現(xiàn)
  4. 最佳實踐:鎖粒度、檢查順序、緩存過期時間

核心要點總結(jié)

設(shè)計點推薦做法原因
鎖粒度按業(yè)務(wù) Key 加鎖避免全局串行化
檢查順序先正常緩存,后空值緩存假設(shè)大部分請求是正常的
空值緩存過期30 分鐘平衡防護效果和數(shù)據(jù)恢復(fù)
鎖類型lock() 阻塞等待保證最終一致性

雙重判定鎖本質(zhì)上是一種減少鎖競爭的優(yōu)化模式。第一次檢查讓大部分請求快速返回,第二次檢查避免重復(fù)查庫。理解了這個核心思想,在其他場景也能靈活運用。

到此這篇關(guān)于Redis雙重判定鎖的實現(xiàn)(緩存擊穿的終極解決方案)的文章就介紹到這了,更多相關(guān)Redis雙重判定鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評論

浮山县| 临武县| 修文县| 徐汇区| 扎囊县| 嘉义市| 遵义市| 金川县| 榕江县| 泾川县| 定结县| 宣威市| 青神县| 禹城市| 连州市| 大渡口区| 堆龙德庆县| 曲阜市| 伊川县| 怀集县| 苍溪县| 溧阳市| 盐池县| 卓尼县| 越西县| 临安市| 陇川县| 台安县| 三原县| 文安县| 永丰县| 长白| 申扎县| 宁国市| 元阳县| 峨眉山市| 蛟河市| 无棣县| 盐山县| 榆树市| 兰考县|