缓存击穿问题也叫热点key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库造成巨大的冲击。 --引用哔哩哔哩UP主“黑马程序员”教程《Redis入门到实战教程》中的PPT内容
常见的解决方案有2中:
1.互斥锁
2.逻辑过期
互斥锁原理示意图(引用B站视频中的PPT):
简单来说,就是线程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);
}
}
后续可将通用的方法抽取出来封装到一个工具类中,至此,代码编写完成,启动服务,清空缓存数据
通过Jmeter工具来进行并发测试,设置100个线程1秒钟跑完
点击start后查看后台日志,发现只查询了一次数据库
刷新缓存,数据已存入