一、缓存击穿

缓存击穿问题也叫热点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后查看后台日志,发现只查询了一次数据库

刷新缓存,数据已存入

 

 

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐