前言
缓存击穿:同一个时刻,大量的请求访问一个热点key,热点key失效的瞬间,持续的大并发会直接请求数据库,就像在屏障上早开了一个洞。
一、解决方案
- 不要设置过期时间
- 加速
二、加锁
没有加锁再看下面一个代码
@RestController
@RequestMapping(value = "/hello")
public class HelloController {
@Autowired
private RedisTemplate<String , String> redisTemplate ;
@GetMapping(value = "/count")
public String count() {
String count = redisTemplate.opsForValue().get("count");
if(StringUtils.isEmpty(count)) {
redisTemplate.opsForValue().set("count" , "1");
}else {
int parseInt = Integer.parseInt(count);
parseInt++ ;
redisTemplate.opsForValue().set("count" , String.valueOf(parseInt));
}
return "ok" ;
}
}
在通过测试攻击发送1000个多线程请求,count结果期望是1000.
结果发现count的结果为27
加锁再测试,代码如下
@RestController
@RequestMapping(value = "/hello")
public class HelloController {
@Autowired
private RedisTemplate<String , String> redisTemplate ;
private Lock lock = new ReentrantLock();
@GetMapping(value = "/count")
public String count() {
//
lock.lock();
String count = redisTemplate.opsForValue().get("count");
if(StringUtils.isEmpty(count)) {
redisTemplate.opsForValue().set("count" , "1");
}else {
int parseInt = Integer.parseInt(count);
parseInt++ ;
redisTemplate.opsForValue().set("count" , String.valueOf(parseInt));
}
lock.unlock();
return "ok" ;
}
}
结果如下,count值正确:1000
三、分布式锁引出
思考一个问题?通过jdk锁上锁的前提是什么?
答:同一个jvm中,即无论是显示锁lock()还是隐式锁synchoronized,只能在同一个jvm上加锁。分布式环境需要分布式锁。
配置两个相同的微服务
再压力测试,结果如下,可以看到锁围起作用,jdk锁对于分布式无效果
四、分布式锁
分布式锁就是在分布式系统环境下提供的一种机制,通过这种锁机制可以保证多个线程在分布式系统环境下操作共享数据的安全性.
原理:在获取锁的时候插入数据,如果插入成功,那就获取了锁,插入失败就获取锁失败,在释放锁的时候需要删除此数据.
redis中提供了一个命令setnx,如果设置的值存在那么就不能再次设置这个值.
删除这个数据,就可以重新设置上去
上锁
package com.atguigu.gmall.item.controller;
import com.alibaba.cloud.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@RestController
@RequestMapping(value = "/hello")
public class HelloController {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@GetMapping(value = "/count")
public String count() {
//先上锁
lock();
//执行业务操作
incr();
//解锁
unlock();
return "ok";
}
//定义加锁方法
public void lock() {
Boolean setIfAbsent = redisTemplate.opsForValue().setIfAbsent("lock", "x");
if(!setIfAbsent){
Boolean setIfAbsent1 = redisTemplate.opsForValue().setIfAbsent("lock", "x");
if(setIfAbsent1){
Boolean setIfAbsent2 = redisTemplate.opsForValue().setIfAbsent("lock", "x");
........//一直尝试获取锁
}
}
}
//定义解锁方法
public void unlock() {
redisTemplate.delete("lock");
}
//定义业务
public void incr() {
String count = redisTemplate.opsForValue().get("count");
if (StringUtils.isEmpty(count)) {
redisTemplate.opsForValue().set("count", "1");
} else {
int parseInt = Integer.parseInt(count);
parseInt++;
redisTemplate.opsForValue().set("count", String.valueOf(parseInt));
}
}
}
将if处判断是否上锁成功换成while() 循环判断
public void lock() {
//没获取锁将会一直尝试向数据库插入数据,插入失败表明没获取到锁,插入成功就不在进入循环,进入下一步的业务操作
while (!redisTemplate.opsForValue().setIfAbsent("lock", "x")) {
}
}
结果正确
思考问题一(加锁):
1、业务异常锁有没有释放,怎么解决? finally
2、释放锁之前服务器宕机了,锁有没有释放怎么解决? 给锁设置过期时间
注意:下面这种设置锁的过期时间,并不能保证其原子性(要么都成功,要么都失败),可以完全避免马上要给锁设置过期时间,服务器宕机了
while (!redisTemplate.opsForValue().setIfAbsent("lock", "x")){ redisTemplate.expire("lock",30,TimeUnit.SECONDS); }
package com.atguigu.gmall.item.controller;
import com.alibaba.cloud.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@RestController
@RequestMapping(value = "/hello")
public class HelloController {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@GetMapping(value = "/count")
public String count() {
//先上锁
lock();
//执行业务操作
try {
incr();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//业务出现异常也可以正常解锁
unlock();
}
return "ok";
}
//定义加锁方法
public void lock() {
//没获取锁将会一直尝试向数据库插入数据,插入失败表明没获取到锁,插入成功就不在进入循环,进入下一步的业务操作
while (!redisTemplate.opsForValue().setIfAbsent("lock", "x", 30, TimeUnit.SECONDS)) //设置过期时间防止将要释放锁的时候服务器宕机
{
}
}
//定义解锁方法
public void unlock() {
redisTemplate.delete("lock");
}
//定义业务
public void incr() {
String count = redisTemplate.opsForValue().get("count");
if (StringUtils.isEmpty(count)) {
redisTemplate.opsForValue().set("count", "1");
} else {
int parseInt = Integer.parseInt(count);
parseInt++;
redisTemplate.opsForValue().set("count", String.valueOf(parseInt));
}
}
}
思考问题二(解锁):一个线程释放了另一个线程的锁
A1线程抢到了锁,A1处理业务需要9.5秒,现在需要解锁,需要0.8秒的链路传输,可是自动释放锁的时间是10s,锁自动释放后A2线程抢到了锁,此时A1线程传输到了,直接释放了A2线程的锁。
解决方案:判断锁是否是自己的
package com.atguigu.gmall.item.controller;
import com.alibaba.cloud.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@RestController
@RequestMapping(value = "/hello")
public class HelloController {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@GetMapping(value = "/count")
public String count() {
//生成线程的唯一标识
String uuid = UUID.randomUUID().toString().replace("-", "");
//先上锁
lock(uuid);
//执行业务操作
try {
incr();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//解锁
unlock(uuid);
}
return "ok";
}
//定义加锁方法
public void lock(String uuid) {
//没获取锁将会一直尝试向数据库插入数据,插入失败表明没获取到锁,插入成功就不在进入循环,进入下一步的业务操作
while (!redisTemplate.opsForValue().setIfAbsent("lock", uuid, 30, TimeUnit.SECONDS)) {
}
}
//定义解锁方法
public void unlock(String uuid) {
//判断是是不是自己的
String lock = redisTemplate.opsForValue().get("lock");
if (lock.equals(uuid)) {
//锁是自己的,释放
redisTemplate.delete(uuid);
}
}
//定义业务
public void incr() {
String count = redisTemplate.opsForValue().get("count");
if (StringUtils.isEmpty(count)) {
redisTemplate.opsForValue().set("count", "1");
} else {
int parseInt = Integer.parseInt(count);
parseInt++;
redisTemplate.opsForValue().set("count", String.valueOf(parseInt));
}
}
}
思考问题三(解锁):一个线程释放了另一个线程的锁
与问题二相似在判断成功后存在延迟,此时锁的自动释放时间到了,另外一个线程加上了锁,此时这个线程释放了别的线程锁.
解决方案:判断和删除成为原子性操作,lua脚本
if redis.call("get",KEYS[1]) == ARGV[1]
then
return redis.call("del",KEYS[1])
else
return 0
end
package com.atguigu.gmall.item.controller;
import com.alibaba.cloud.commons.lang.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@RestController
@RequestMapping(value = "/hello")
@Slf4j
public class HelloController {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@GetMapping(value = "/count")
public String count() {
//生成线程的唯一标识
String uuid = UUID.randomUUID().toString().replace("-", "");
//先上锁
lock(uuid);
//执行业务操作
try {
incr();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//解锁
unlock(uuid);
}
return "ok";
}
//定义加锁方法
public void lock(String uuid) {
//没获取锁将会一直尝试向数据库插入数据,插入失败表明没获取到锁,插入成功就不在进入循环,进入下一步的业务操作
while (!redisTemplate.opsForValue().setIfAbsent("lock", uuid, 30, TimeUnit.SECONDS)) {
}
}
//定义解锁方法
public void unlock(String uuid) {
/**
* 使用lua脚本
*/
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
"then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end" ;
Long execute = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList("lock"), uuid);
if (execute == 0){
log.info("锁是别人的,释放失败。。。。。。");
}else {
log.info("释放成功............");
}
}
//定义业务
public void incr() {
String count = redisTemplate.opsForValue().get("count");
if (StringUtils.isEmpty(count)) {
redisTemplate.opsForValue().set("count", "1");
} else {
int parseInt = Integer.parseInt(count);
parseInt++;
redisTemplate.opsForValue().set("count", String.valueOf(parseInt));
}
}
}
思考问题三(解锁):业务没有完成,但是锁释放了
解决方案:1.大量测试得到业务需要的时间
2.看门狗机制
package com.atguigu.gmall.item.controller;
import com.alibaba.cloud.commons.lang.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@RestController
@RequestMapping(value = "/hello")
@Slf4j
public class HelloController {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@GetMapping(value = "/count")
public String count() {
//生成线程的唯一标识
String uuid = UUID.randomUUID().toString().replace("-", "");
//先上锁
lock(uuid);
//执行业务操作
try {
incr();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//解锁
unlock(uuid);
}
return "ok";
}
//定义加锁方法
public void lock(String uuid) {
//没获取锁将会一直尝试向数据库插入数据,插入失败表明没获取到锁,插入成功就不在进入循环,进入下一步的业务操作
while (!redisTemplate.opsForValue().setIfAbsent("lock", uuid, 30, TimeUnit.SECONDS)) {
}
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(10);
/**
* Runnable command, 要执行的任务
* long initialDelay, 第一次执行该任务的时间间隔
* long period, 周期性的执行该任务的时间间隔
* TimeUnit unit 时间单位
*/
scheduledThreadPool.scheduleAtFixedRate(() -> {
redisTemplate.expire("lock", 30, TimeUnit.SECONDS);
}, 10, 10, TimeUnit.SECONDS);
}
//定义解锁方法
public void unlock(String uuid) {
/**
* 使用lua脚本
*/
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
"then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
Long execute = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList("lock"), uuid);
if (execute == 0) {
log.info("锁是别人的,释放失败。。。。。。");
} else {
log.info("释放成功............");
}
}
//定义业务
public void incr() {
try {
TimeUnit.SECONDS.sleep(60);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
String count = redisTemplate.opsForValue().get("count");
if (StringUtils.isEmpty(count)) {
redisTemplate.opsForValue().set("count", "1");
} else {
int parseInt = Integer.parseInt(count);
parseInt++;
redisTemplate.opsForValue().set("count", String.valueOf(parseInt));
}
}
}