日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

Redis緩存擊穿解決方案之互斥鎖

作者:wl_Honest 更新時間: 2022-10-14 編程語言

一、緩存擊穿

緩存擊穿問題也叫熱點key問題,就是一個被高并發訪問并且緩存重建業務較復雜的key突然失效了,無數的請求訪問會在瞬間給數據庫造成巨大的沖擊。 --引用嗶哩嗶哩UP主“黑馬程序員”教程《Redis入門到實戰教程》中的PPT內容

常見的解決方案有2中:

1.互斥鎖

2.邏輯過期

二、互斥鎖

互斥鎖原理示意圖(引用B站視頻中的PPT):

簡單來說,就是線程1查詢緩存未命中,這時它會去獲取互斥鎖,然后查詢數據庫獲取結果并將結果寫入緩存中,最后釋放鎖。在線程1釋放鎖之前,其它線程都不能獲取鎖,只能睡眠一段時間后重試,如果能命中緩存,則返回數據,否則繼續嘗試獲取互斥鎖。

該解決方案的優點

1.沒有額外的內存消耗

2.保證一致性

3.實現簡單

缺點:

1.線程需要等待,性能受到影響

2.可能有死鎖的風險

三、代碼示例

現根據B站視頻中的例子,自己參考寫一個互斥鎖的示例,根據城市行政區劃代碼查詢城市信息。

首先放出maven依賴,可根據自己的實際情況做增減:

<dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--集成mysql數據庫-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
	    <groupId>org.springframework.boot</groupId>
	    <artifactId>spring-boot-starter-jdbc</artifactId>
	</dependency>
	<dependency>
	    <groupId>org.springframework</groupId>
	    <artifactId>spring-jdbc</artifactId>
	    <version>5.1.5.RELEASE</version>
	</dependency>
	<dependency>
	    <groupId>org.springframework</groupId>
	    <artifactId>spring-beans</artifactId>
	</dependency>
    <!-- mybatis-plus -->
    <dependency>
	  	<groupId>com.baomidou</groupId>
	  	<artifactId>mybatis-plus-boot-starter</artifactId>
	  	<version>3.4.1</version>
	</dependency>
	<!--springboot中的redis依賴-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <!-- lettuce pool 緩存連接池-->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
    </dependency>
    <dependency>
	    <groupId>commons-lang</groupId>
	    <artifactId>commons-lang</artifactId>
	    <version>2.6</version>
	</dependency>
	<!--處理JSON格式-->
	<dependency>
		<groupId>com.alibaba</groupId>
		<artifactId>fastjson</artifactId>
		<version>1.2.3</version>
	</dependency>
	  <!--hutool-->
	  <dependency>
		  <groupId>cn.hutool</groupId>
		  <artifactId>hutool-all</artifactId>
		  <version>5.7.17</version>
	  </dependency>
	<dependency>
	    <groupId>org.projectlombok</groupId>
	    <artifactId>lombok</artifactId>
	    <version>1.16.20</version>
	</dependency>
	<dependency>  
	    <groupId>org.springframework.boot</groupId>  
	    <artifactId>spring-boot-starter-web</artifactId>  
	    <exclusions><!-- 去掉springboot默認配置 -->  
	        <exclusion>  
	            <groupId>org.springframework.boot</groupId>  
	            <artifactId>spring-boot-starter-logging</artifactId>  
	        </exclusion>  
	    </exclusions>  
	</dependency>

	<dependency> <!-- 引入log4j2依賴 -->
	    <groupId>org.springframework.boot</groupId>
	    <artifactId>spring-boot-starter-log4j2</artifactId>
	</dependency>

	  <!-- swagger -->
	  <dependency>
		  <groupId>io.springfox</groupId>
		  <artifactId>springfox-boot-starter</artifactId>
		  <version>3.0.0</version>
	  </dependency>
  </dependencies>

配置文件:

server:
  port: 8000

spring:
  application:
    name: my_web
  datasource:
    driverClassName: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/my_web?characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: xxxxxx
  redis:
    host: 127.0.0.1
    port: 6379
    lettuce:
      pool:
        max-active: 100
        max-wait: 1
        max-idle: 10
        min-idle: 0
    timeout: 1000

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

在編寫邏輯代碼前,事先準備幾個常量,放入一個常量類中:

package com.wl.standard.common.result.constants;

/**
 * redis常量
 * @author wl
 * @date 2022/3/17 16:09
 */
public interface RedisConstants {
    /**
     * 空值緩存過期時間(分鐘)
     */
    Long CACHE_NULL_TTL = 2L;

    /**
     * 城市redis緩存key
     */
    String CACHE_CITY_KEY = "cache:city:";
    /**
     * 城市redis緩存過期時間(分鐘)
     */
    Long CACHE_CITY_TTL = 30L;

    /**
     * 城市redis互斥鎖key
     */
    String LOCK_CITY_KEY = "lock:city:";
}

?

?Controller層:

package com.wl.standard.controller;

import com.wl.standard.common.result.HttpResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import com.wl.standard.service.CityService;

/**
 * @author wl
 * @date 2021/11/18
 */
@Api(tags = "城市管理接口")
@RestController
@RequestMapping("/city")
public class CityController {

	private final CityService cityService;

	@Autowired
	public CityController(CityService cityService) {
		this.cityService = cityService;
	}

	@GetMapping("/{id}")
	public HttpResult getCity(@PathVariable("id") String cityCode) {
		return HttpResult.success(cityService.getByCode(cityCode));
	}
}

Service層實現類:

編寫查詢邏輯前,先定義好獲取互斥鎖和釋放鎖的方法:

/**
     * 獲取互斥鎖
     * @return
     */
    private Boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtils.isTrue(flag);
    }

    /**
     * 釋放鎖
     * @param key
     */
    private void unLock(String key) {
        stringRedisTemplate.delete(key);
    }

其中,獲取互斥鎖和釋放鎖的傳參都應傳城市redis互斥鎖key

然后編寫通過互斥鎖機制查詢城市信息的方法:

/**
     * 通過互斥鎖機制查詢城市信息
     * @param key
     */
    private City queryCityWithMutex(String key, String cityCode) {
        City city = null;
        // 1.查詢緩存
        String cityJson = stringRedisTemplate.opsForValue().get(key);
        // 2.判斷緩存是否有數據
        if (StringUtils.isNotBlank(cityJson)) {
            // 3.有,則返回
            city = JSONObject.parseObject(cityJson, City.class);
            return city;
        }
        // 4.無,則獲取互斥鎖
        String lockKey = RedisConstants.LOCK_CITY_KEY + cityCode;
        Boolean isLock = tryLock(lockKey);
        // 5.判斷獲取鎖是否成功
        try {
            if (!isLock) {
                // 6.獲取失敗, 休眠并重試
                Thread.sleep(100);
                return queryCityWithMutex(key, cityCode);
            }
            // 7.獲取成功, 查詢數據庫
            city = baseMapper.getByCode(cityCode);
            // 8.判斷數據庫是否有數據
            if (city == null) {
                // 9.無,則將空數據寫入redis
                stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
            // 10.有,則將數據寫入redis
            stringRedisTemplate.opsForValue().set(key, JSONObject.toJSONString(city), RedisConstants.CACHE_CITY_TTL, TimeUnit.MINUTES);
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            // 11.釋放鎖
            unLock(lockKey);
        }
        // 12.返回數據
        return city;
    }

Service層實現類完整代碼:

package com.wl.standard.service.impl;

import cn.hutool.core.bean.BeanUtil;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.wl.standard.common.result.constants.RedisConstants;
import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import com.wl.standard.mapper.CityMapper;
import com.wl.standard.entity.City;
import com.wl.standard.service.CityService;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

/**
 * @author wl
 * @date 2021/11/18
 */
@Service
@Slf4j
public class CityServiceImpl extends ServiceImpl<CityMapper, City> implements CityService{

    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    public CityServiceImpl(StringRedisTemplate stringRedisTemplate){
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public City getByCode(String cityCode) {
        String key = RedisConstants.CACHE_CITY_KEY+cityCode;
        return queryCityWithMutex(key, cityCode);
    }

    /**
     * 通過互斥鎖機制查詢城市信息
     * @param key
     */
    private City queryCityWithMutex(String key, String cityCode) {
        City city = null;
        // 1.查詢緩存
        String cityJson = stringRedisTemplate.opsForValue().get(key);
        // 2.判斷緩存是否有數據
        if (StringUtils.isNotBlank(cityJson)) {
            // 3.有,則返回
            city = JSONObject.parseObject(cityJson, City.class);
            return city;
        }
        // 4.無,則獲取互斥鎖
        String lockKey = RedisConstants.LOCK_CITY_KEY + cityCode;
        Boolean isLock = tryLock(lockKey);
        // 5.判斷獲取鎖是否成功
        try {
            if (!isLock) {
                // 6.獲取失敗, 休眠并重試
                Thread.sleep(100);
                return queryCityWithMutex(key, cityCode);
            }
            // 7.獲取成功, 查詢數據庫
            city = baseMapper.getByCode(cityCode);
            // 8.判斷數據庫是否有數據
            if (city == null) {
                // 9.無,則將空數據寫入redis
                stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
            // 10.有,則將數據寫入redis
            stringRedisTemplate.opsForValue().set(key, JSONObject.toJSONString(city), RedisConstants.CACHE_CITY_TTL, TimeUnit.MINUTES);
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            // 11.釋放鎖
            unLock(lockKey);
        }
        // 12.返回數據
        return city;
    }

    /**
     * 獲取互斥鎖
     * @return
     */
    private Boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtils.isTrue(flag);
    }

    /**
     * 釋放鎖
     * @param key
     */
    private void unLock(String key) {
        stringRedisTemplate.delete(key);
    }
}

后續可將通用的方法抽取出來封裝到一個工具類中,至此,代碼編寫完成,啟動服務,清空緩存數據

?通過Jmeter工具來進行并發測試,設置100個線程1秒鐘跑完

?

?點擊start后查看后臺日志,發現只查詢了一次數據庫

刷新緩存,數據已存入

?

?

原文鏈接:https://blog.csdn.net/wl_Honest/article/details/123556902

欄目分類
最近更新