请大家了解下redisson,在之前讲过的分布式锁和下面讲的布隆过滤器还有读写锁都需要使用到redisson.
缓存穿透
当访问一个不存在的key,会导致穿过缓存直击数据库,这个时候如果有黑客恶意使用压测工具一直访问,导致数据库压力过打崩溃。
解决方案:对不存在的空值进行缓存,给定一个过期时间,防止短时间内恶意攻击。但是还是会恶意输入大量不存在的key,还是会查询一次数据库,那么可以在访问缓存前使用布隆过滤器进行拦截。
布隆过滤器:通过多种算法hash取模后把某位置为1,多个位联合起来判断都为1就表示存在,布隆说存在可能不一定存在,但是说不存在就一定不存在。我们可以在项目启动前将所有要缓存的数据加入到布隆过滤器,业务中也可以天假新的数据到布隆过滤器,查询时通过布隆判断是否存在,不存在直接返回。其实布隆过滤器也有个缺点,不能删除数据,只能重新初始化,时间可以选在凌晨没人使用的时候开始。
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
//构造Redisson
RedissonClient redisson = Redisson.create(config);
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("nameList");
//初始化布隆过滤器:预计元素为100000000L,误差率为3%,根据这两个参数会计算出底层的bit数组大小
bloomFilter.tryInit(100000L,0.03);
//将数据插入到布隆过滤器中
bloomFilter.add("key1");
bloomFilter.add("key2");
bloomFilter.add("key3");
//判断下面号码是否在布隆过滤器中
System.out.println(bloomFilter.contains("key1"));//false
System.out.println(bloomFilter.contains("key2"));//false
System.out.println(bloomFilter.contains("key4"));//true
}
缓存击穿
大量的key在同一时刻失效,大量的存在的key请求也还是直击数据库造成压力过大
解决方案就是让数据的缓存时间分散不要再同一时刻失效,热点数据让其用不失效
至此以上2个场景伪代码如下:
public class RedissonBloomFilter {
private static RedisTemplate redisTemplate;
static RBloomFilter<String> bloomFilter;
public static void main(String[] args) {
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
//构造Redisson
RedissonClient redisson = Redisson.create(config);
bloomFilter = redisson.getBloomFilter("nameList");
//初始化布隆过滤器:预计元素为100000000L,误差率为3%,根据这两个参数会计算出底层的bit数组大小
bloomFilter.tryInit(100000L, 0.03);
// 从查询数据库所有要缓存的数据
List<Map> keys = null;
keys = DAOkeys();
Iterator<Map> iterator = keys.iterator();
//初始化所有的数据
if (iterator.hasNext()){
Map key = iterator.next();
// 放入布隆过滤器
bloomFilter.add(key.id);
// 放入缓存
redisTemplate.opsForValue().set(key.id,key);
}
}
Object get(String id) {
// 先去判断是在布隆过滤器中
boolean contains = bloomFilter.contains(id);
if (!contains) {
return "不存在的数据";
} else {
// 查询缓存
Object o = redisTemplate.opsForValue().get(id);
if (StringUtils.isEmpty(o)){
// 查询数据库
Object obj= DAOById(id);
//加入到缓存
redisTemplate.opsForValue().set(id,obj);
//如果查询出的数据为空天假一个较短的过期时间,解决存在的key但是value没值
if (obj==null){
// 加上一个随机的过期时间
int time= new Random().nextInt(1000)+1000;
redisTemplate.expire(id,time, TimeUnit.SECONDS);
}
return o;
}else{
return o;
}
}
}
}
缓存雪崩
缓存层支持不住或得宕机后导致所有数据直击数据库
解决方案:1.缓存需要支持高可用,使用redis sentinel哨兵模式或者redis culster集群模式
2.后端做限流熔断并降级,限流降级组件sentinel或hystrix
热点缓存key重建优化
热点数据重建缓存一般要求给永不失效,如果失效时间到了,并发下过多线程过来创建缓存,可以使用redis的分布式锁来保证一直有一个线程可以创建缓存成功,其他没有拿到锁的线程睡眠几毫秒后重新获取缓存。
热点缓存key重建优化
不一致,读写不一致)双写是指在写入数据库的时候同步更新缓存
读写是指在写数据库的时候删除缓存,在读数据库的时候写入缓存
一般推荐第二中。但是从图上可以发现在并发下都有问题出现。
解决方案:通过加读写锁保证并发读写和写写按照顺序排好队串行执行,读读的时候相当于无锁。(注意lock()无参方法加锁没有获取到,会一直自旋等待)
package com.redisson;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class IndexController {
@Autowired
private Redisson redisson;
@Autowired
private StringRedisTemplate stringRedisTemplate;
private volatile int count;
@RequestMapping("/deduct_stock")
/*
问题:对于高并发场景,为了保证不会超卖,数据一致性
1.synchronized对代码枷锁,但是只能解决在同一个 进程 单机应用的情况,粒度不够,如果分布式部署不可行
// synchronized (this){}
2.可以使用setnx分布式锁对每个线程进心控制,粒度细可行。
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "zhuge"); //jedis.setnx(k,v)
当没有加锁成功直接返回前端错误码,成功就可以直接进行库存操作
if (!result) {
return "error_code";
}
然后在finally里面将锁释放,避免程序报错,锁不释放问题
stringRedisTemplate.delete(lockKey);
2.1 考虑到如果程序不报错,而是直接崩溃,比如运维直接 kill 进程 ,而不是shutdown,那么还是会死锁,所以解决方案是加个过期时间
******这个锁加上了会有严重的超卖问题,枷锁的意义只为解决宕机锁宕机不是放问题,但是应用跑得慢可是锁时间短,会出现锁释放库存还没操作完,其他线程拿到锁再次操作和锁被其他线程释放,******
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "zhuge"); //jedis.setnx(k,v)
stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);
2.2 为了保证原子性(其实就是钻牛角尖,担心在加过期时间前就崩了), 在设置key时就加过期时间
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "value", 30, TimeUnit.SECONDS);
3.控制每个线程是否释放当前锁,虽然力度够细,
有一个前提需要知道,加锁是在第三方客户端redis上
在没有加过期时间会出现死锁,但是可以保证该主线程操作库存完成后准确将该锁释放
但是为了解决当程序出现的宕机问题加了过期时间,但是加了过期时间出现另一个问题,
如果应用程序还没跑完,也就是库存操作还没操作完,过期时间却到了,下一个线程就可以拿到锁然后开始操作,这时我上一个线程完成了库存操作,
然后释放锁,这个时候该线程的锁早已经因为过期时间到了已经释放了,那么你现在释放的锁是就是释放下一个线程的锁。以此往复出现超卖。
需要解决问题一:保证在加了过期时间的前提下又能准确控制当前线程释放自己加的锁
setnx只对key加锁,定义一个随机的字符串作为加锁的值交给加锁的ky
String clientId = UUID.randomUUID().toString();
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);
然后再finally里面去一下这个值,判断当前线程拿到的值是当前线程生成的局部变量就进行删除
if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
stringRedisTemplate.delete(lockKey);
}
需要解决问题二:保证在加了过期时间的前提下,又要保证库库存操作完才将锁释放。锁续命,给一个子线程定时任务iterm,
每过一段时间看下主线程是否执行完,没有就重新在加一个过期时间
解觉方案:
有一个实现好了的客户端工具包
导入
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.5</version>
</dependency>
注册
@Bean
public Redisson redisson() {
// 此为单机模式
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.0.60:6379").setDatabase(0);
return (Redisson) Redisson.create(config);
}
使用,类似setnx
RLock redissonLock = redisson.getLock(lockKey);
//加锁
redissonLock.lock(); //setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);
//解锁
redissonLock.unlock();
*/
public String deductStock() {
String lockKey = "product_101";
RLock redissonLock = redisson.getLock(lockKey);
try {
//加锁
redissonLock.lock(); //setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);
// synchronized (this){ 方案一加synchronized,但是只能解决在同一个进程单机应用的情况,如果分布式部署不可行
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
count++;
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足,总计卖出"+count);
}
// }
} finally {
redissonLock.unlock();
/*if (lock && clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
stringRedisTemplate.delete(lockKey);
}*/
}
return "end";
}
// @RequestMapping("/redlock")
// public String redlock() {
// String lockKey = "product_001";
// //这里需要自己实例化不同redis实例的redisson客户端连接,这里只是伪代码用一个redisson客户端简化了
// RLock lock1 = redisson.getLock(lockKey);
// RLock lock2 = redisson.getLock(lockKey);
// RLock lock3 = redisson.getLock(lockKey);
//
// /**
// * 根据多个 RLock 对象构建 RedissonRedLock (最核心的差别就在这里)
// */
// RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
// try {
// /**
// * waitTimeout 尝试获取锁的最大等待时间,超过这个值,则认为获取锁失败
// * leaseTime 锁的持有时间,超过这个时间锁会自动失效(值应设置为大于业务处理的时间,确保在锁有效期内业务能处理完)
// */
// boolean res = redLock.tryLock(10, 30, TimeUnit.SECONDS);
// if (res) {
// //成功获得锁,在这里处理业务
// }
// } catch (Exception e) {
// throw new RuntimeException("lock fail");
// } finally {
// //无论如何, 最后都要解锁
// redLock.unlock();
// }
//
// return "end";
// }
//
@RequestMapping("/get_stock")
public String getStock(@RequestParam("clientId") Long clientId) throws InterruptedException {
String lockKey = "product_stock_101";
RReadWriteLock readWriteLock = redisson.getReadWriteLock(lockKey);
RLock rLock = readWriteLock.readLock();
String stock="";
rLock.lock();
System.out.println("获取读锁成功:client=" + clientId);
stock= stringRedisTemplate.opsForValue().get("stock");
if (StringUtils.isEmpty(stock)) {
System.out.println("查询数据库库存为10。。。");
Thread.sleep(5000);
stringRedisTemplate.opsForValue().set("stock", "10");
}
rLock.unlock();
System.out.println("释放读锁成功:client=" + clientId);
return stock;
}
@RequestMapping("/update_stock")
public String updateStock(@RequestParam("clientId") Long clientId) throws InterruptedException {
String lockKey = "product_stock_101";
RReadWriteLock readWriteLock = redisson.getReadWriteLock(lockKey);
RLock writeLock = readWriteLock.writeLock();
writeLock.lock();
System.out.println("获取写锁成功:client=" + clientId);
System.out.println("修改商品101的数据库库存为6。。。");
stringRedisTemplate.delete("stock");
int i=1/0;
writeLock.unlock();
System.out.println("释放写锁成功:client=" + clientId);
return "end";
}
}