文章目录
1、缓存击穿
- 是某一个热点key在高并发访问的情况下,突然失效,导致大量的并发打进mysql数据库的情况
- 解决:在正常的访问情况下,如果缓存失效,需保护mysql,在重启缓存的过程,
使用redis数据库的分布式锁,解决mysql的访问压力问题。
2、缓存穿透
- 是利用redis和mysql的机制(redis缓存一旦不存在,就访问mysql),直接绕过缓存访问mysql,而制造的db请求压力
一般在代码中防止该现象的发生 - 解决: 为了防止缓存穿透将,null或者空字符串值设置给redis
3、缓存雪崩
- 缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,导致的db崩溃
- 解决:设置不同的缓存失效时间
4、解决缓存击穿的分布式锁有两种
1. redis自带一个分布式锁,set px nx
- 这种分布式锁作用在redis上
- redis上的命令:set key:lock 1 px time(ms) nx
2. redisson框架,一个redis的带有juc的lock功能的客户端的实现(既有jedis的功能,又有juc的锁功能)
- 这种分布式锁作用在多个Jedis上
3.使用redis分布式锁的实现
3.1、使用本方法处理高并发下的人会遇到的问题
1、 高并发下,若是拿锁的线程因某些原因,导致自己的锁未经过自己释放而过期,而某一时刻该线程醒来时,会来释放锁,而此时redis上的锁已经是别的线程上的锁了,怎么办?
解决此问题有两种方案:
-
- 不主动释放锁,让其自动释放。(但要求高响应的系统都不会这样实现)
-
- 仍然主动释放锁,但是设置锁的key对于的唯一Value ,需解锁时,先判断是否是自己的锁,再释放锁,是则释放,否则直接跳过。
2、若是刚好在比对锁的key对于的唯一Value的时候,该线程的锁过期,由别的线程进入并拿到锁,此时释放锁,而此时redis上的锁已经是别的线程上的锁了,该怎么办?
- 可以用lua脚本,在查询到key的同时删除该key,防止高并发下的该意外的发生。
(lua:别让我看到你,看到我就把你干掉)
3.2、redis分布式锁代码体现,以下代码对上述问题的解决方案都有体现。
本代码是一个业务逻辑中的服务层的一个方法。
public PmsSkuInfo getSkuBySkuId(String skuId,String ip) {
System.out.println("ip为"+ip+"的同学:"+Thread.currentThread().getName()+"进入的商品详情的请求");
//获取jedis对象
Jedis jedis = redisUtil.getJedis();
//获取skuinfo
PmsSkuInfo pmsSkuInfo = new PmsSkuInfo();
//构造key
String skukey = "sku:"+skuId+":Info";
//查缓存
String skuJson = jedis.get(skukey);
//判断缓存是否存在
if (StringUtils.isNotBlank(skuJson)){
//缓存存在 提取出数据封装在对象中
pmsSkuInfo = JSON.parseObject(skuJson,PmsSkuInfo.class);
}else{
//缓存中不存在改数据 去mysql中查找
//设置分布式锁 防止击穿
//设置redis上分布式锁的唯一值
String lockValue = UUID.randomUUID().toString();
//上锁成功放回 OK
String Ok = jedis.set("sku:"+skuId+":lock",lockValue,"nx","px",10*1000);
if (StringUtils.isNotBlank(Ok) && Ok.equals("OK")){
//设置锁成功 有权在10s内操作mysql
pmsSkuInfo = getSkuBySkuIdFromDb(skuId);
if (pmsSkuInfo != null){
//先睡眠5s再让其操作mysql 再解锁
// 睡眠时为了看到其他线程自旋效果 生产环境不会睡眠
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//db存在数据 将db数据存入redis
jedis.set("sku:"+skuId+":Info", JSON.toJSONString(pmsSkuInfo));
}else{
//db不存在数据 将null存入redis 防止穿透
jedis.set("sku:"+skuId+":Info",JSON.toJSONString(""));
}
//操作完mysql,将redis的分布式锁释放
System.out.println("ip为"+ip+"的同学:"+Thread.currentThread().getName()+"使用完毕,将锁归还:"+"sku:" + skuId + ":lock");
String currentLockValue = jedis.get("sku:"+skuId+":lock");
if (StringUtils.isNotBlank(currentLockValue) && currentLockValue.equals(lockValue)){
//jedis.eval("lua");可与用lua脚本,在查询到key的同时删除该key,防止高并发下的意外的发生
jedis.del("sku:" + skuId + ":lock");// 用token确认删除的是自己的sku的锁
}
}else {
//设置失败 说明已经有别的用户线程设置了锁
System.out.println("ip为"+ip+"的同学:"+Thread.currentThread().getName()+"没有拿到锁,开始自旋");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//自旋 让其睡眠几秒后重写调用本方法 睡眠是为了看效果 具体生产环境不会设置睡眠
//注意 不能不带return ,若是不带return 直接调用方法,会另外启动一个线程
//被新启动的线程为孤儿线程,别的线程无法访问
return getSkuBySkuId(skuId,ip);
}
}
//关闭jedis
jedis.close();
return pmsSkuInfo;
}
3.1.1、模拟并发,两个页面同时访问,看后台输出,第一个先进入的线程会拿到锁,锁是唯一,别的线程拿不到锁会不断自旋,直到拿锁线程释放锁为止。
ip为127.0.0.1的同学:DubboServerHandler-192.168.154.1:53630-thread-2进入的商品详情的请求
ip为127.0.0.1的同学:DubboServerHandler-192.168.154.1:53630-thread-2拿到分布式锁
ip为127.0.0.1的同学:DubboServerHandler-192.168.154.1:53630-thread-3进入的商品详情的请求
ip为127.0.0.1的同学:DubboServerHandler-192.168.154.1:53630-thread-3没有拿到锁,开始自旋
ip为127.0.0.1的同学:DubboServerHandler-192.168.154.1:53630-thread-3进入的商品详情的请求
ip为127.0.0.1的同学:DubboServerHandler-192.168.154.1:53630-thread-3没有拿到锁,开始自旋
ip为127.0.0.1的同学:DubboServerHandler-192.168.154.1:53630-thread-2使用完毕,将锁归还:sku:115:lock
ip为127.0.0.1的同学:DubboServerHandler-192.168.154.1:53630-thread-3进入的商品详情的请求
4、使用redission的简单实现
redisson封装许多juc的并发工具类,可以调用内部工具类实现并发控制。
public String testRedisson(){
Jedis jedis = redisUtil.getJedis();
RLock lock = redissonClient.getLock("lock");// 声明锁
lock.lock();//上锁
try {
String v = jedis.get("k");
if (StringUtils.isBlank(v)) {
v = "1";
}
System.out.println("->" + v);
jedis.set("k", (Integer.parseInt(v) + 1) + "");
}finally {
jedis.close();
lock.unlock();// 解锁
}
return "success";
}
有兴趣的小伙伴可以 一起交流哦!!!