Redis缓存击穿解决方案之互斥锁

一、缓存击穿

缓存击穿问题也叫热点key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库造成巨大的冲击。 --引用哔哩哔哩UP主“黑马程序员”教程《Redis入门到实战教程》中的PPT内容

常见的解决方案有2中:

1.互斥锁

2.逻辑过期

二、互斥锁

互斥锁原理示意图(引用B站视频中的PPT):

Redis缓存击穿解决方案之互斥锁_第1张图片

简单来说,就是线程1查询缓存未命中,这时它会去获取互斥锁,然后查询数据库获取结果并将结果写入缓存中,最后释放锁。在线程1释放锁之前,其它线程都不能获取锁,只能睡眠一段时间后重试,如果能命中缓存,则返回数据,否则继续尝试获取互斥锁。

该解决方案的优点

1.没有额外的内存消耗

2.保证一致性

3.实现简单

缺点:

1.线程需要等待,性能受到影响

2.可能有死锁的风险

三、代码示例

现根据B站视频中的例子,自己参考写一个互斥锁的示例,根据城市行政区划代码查询城市信息。

首先放出maven依赖,可根据自己的实际情况做增减:


    
      junit
      junit
      3.8.1
      test
    
    
        org.springframework.boot
        spring-boot-starter-web
    
    
    
        mysql
        mysql-connector-java
    
    
	    org.springframework.boot
	    spring-boot-starter-jdbc
	
	
	    org.springframework
	    spring-jdbc
	    5.1.5.RELEASE
	
	
	    org.springframework
	    spring-beans
	
    
    
	  	com.baomidou
	  	mybatis-plus-boot-starter
	  	3.4.1
	
	
    
        org.springframework.boot
        spring-boot-starter-data-redis
    
    
    
        org.apache.commons
        commons-pool2
    
    
	    commons-lang
	    commons-lang
	    2.6
	
	
	
		com.alibaba
		fastjson
		1.2.3
	
	  
	  
		  cn.hutool
		  hutool-all
		  5.7.17
	  
	
	    org.projectlombok
	    lombok
	    1.16.20
	
	  
	    org.springframework.boot  
	    spring-boot-starter-web  
	      
	          
	            org.springframework.boot  
	            spring-boot-starter-logging  
	          
	      
	

	 
	    org.springframework.boot
	    spring-boot-starter-log4j2
	

	  
	  
		  io.springfox
		  springfox-boot-starter
		  3.0.0
	  
  

配置文件:

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 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);
    }
}

后续可将通用的方法抽取出来封装到一个工具类中,至此,代码编写完成,启动服务,清空缓存数据

Redis缓存击穿解决方案之互斥锁_第2张图片

 通过Jmeter工具来进行并发测试,设置100个线程1秒钟跑完

 Redis缓存击穿解决方案之互斥锁_第3张图片

 点击start后查看后台日志,发现只查询了一次数据库

Redis缓存击穿解决方案之互斥锁_第4张图片

刷新缓存,数据已存入

Redis缓存击穿解决方案之互斥锁_第5张图片 

 

你可能感兴趣的:(java,redis,缓存,redis,数据库)