为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。而 db 承担数据落盘工作(持久化工作),数据库查询一次后将数据存入缓存,以后需要该数据直接从缓存中取。
适合放入缓存的数据:
缓存中存放的所有对象都应该是 JSON 字符串,JSON 跨语言、跨平台兼容。
给缓存中存放 JSON 字符串,从缓存中拿出的 JSON 字符串,还要逆转为能用的对象类型【序列化与反序列化的过程】
Demo:
@Override
public Map<String, List<Catalog2Vo>> getCatalogJson() {
// 给缓存中存放 JSON 字符串,从缓存中拿出的 JSON 字符串,还要逆转为能用的对象类型【序列化与反序列化的过程】
// 1、加入缓存逻辑,缓存中存放的所有对象都应该是 JSON 字符串。
// JSON 跨语言、跨平台兼容
String catalogJson = redisTemplate.opsForValue().get("catalogJson");
if (StringUtils.isEmpty(catalogJson)) {
// 2、缓存中没有该数据,就从数据库查询出来
// getCatalogJsonFromDb() 该方法是涉及数据库操作
Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
// 3、将从数据库中查出来的 catalogJsonFromDb 对象转成 JSON,再存放入缓存中
// 关于 JSON 与 Object 的转换,在 Spring MVC 的笔记中也有记录
String jsonString = JSON.toJSONString(catalogJsonFromDb);
redisTemplate.opsForValue().set("catalogJson",jsonString);
return catalogJsonFromDb;
}
// 转为指定的对象
// protected TypeReference() {} 底层是受保护的,因此这里引用的时候使用匿名内部类的方式加以实现
Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJson,new TypeReference<Map<String, List<Catalog2Vo>>>(){});
return result;
}
产生堆外内存溢出:OutOfDirectMemoryError
1)、Spring Boot 2.0 以后默认使用 lettuce 作为操作 redis 的客户端,它使用 netty 进行网络通信
2)、lettuce 的 bug 导致 netty 堆外内存溢出 (现在应该修复了,因为压测结果异常率为0 没有增加)
netty 如果没有指定堆外内存,默认使用 -Xmx100m 作为堆外内存,可以通过 -Dio.netty.maxDirectMemory 进行设置
解决方案:不能使用 -Dio.netty.maxDirectMemory 只去调大堆外内存,因为调大最大内存只是延缓了该异常,在长久运行累积之后堆外内存溢出还会产生,那就应该通过如下解决:
1)、升级 lettuce 客户端
2)、切换使用 jedis 客户端
另外,lettuce 和 jedis 是操作 redis 的底层客户端,本身封装了操作 redis 的一些 API。Spring 为了简化将这些进行再次封装成
RedisTemplate
缓存穿透:
指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的 null 写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义
风险:
利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃
解决:
null 结果缓存,并加入短暂过期时间
缓存雪崩:
指在我们设置缓存时 key 采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到 DB,DB 瞬时压力过重导致雪崩
解决:
原有的失效时间基础上增加一个随机值,比如 1~5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件
缓存击穿:
解决:
加锁 —— 大量并发只让一个人去查,其它人等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用再去查 DB
我们可以整合 redisson 作为分布式锁等功能框架
引入依赖
<dependency>
<groupId>org.redissongroupId>
<artifactId>redissonartifactId>
<version>3.15.2version>
dependency>
配置 redisson
@Configuration
public class MyRedissonConfig {
/**
* 所有对 Redisson 的使用都是通过 RedissonClient 对象
* @return
* @throws IOException
*/
@Bean(destroyMethod="shutdown")
public RedissonClient redisson() throws IOException {
// 1、创建配置
// 可以用"rediss://"来启用SSL连接
// Redis url should start with redis:// or rediss://
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.56.10:6379");
// 2、根据 config 创建出 RedissonClient 实例
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
// 1、获取一把锁,只要锁的名字一样,就是同一把锁
RLock lock = redisson.getLock("my-lock");
// 2、加锁
lock.lock(); // 阻塞式等待。默认加的锁都是 30s 时间。
// 1)、提供了锁的自动续期,如果业务超长,运行期间自动给锁续上新的 30s。不用担心业务时间长,锁自动过期被删掉
// 2)、加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认也会在 30s 以后自动删除
try {
System.out.println("加锁成功,执行业务...." + Thread.currentThread().getId());
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
} finally {
// 3、解锁
System.out.println("释放锁..." + Thread.currentThread().getId());
lock.unlock();
}
lock.lock(10, TimeUnit.SECONDS); // 10s 自动解锁,自动解锁时间一定要大于业务的执行时间
lock.lock(10, TimeUnit.SECONDS); 在锁时间到了以后,就不会自动续期,如果业务时间过长大于自动解锁时间 如 30s,第一个线程的锁 10s 后过期解锁,下一个线程就会进来占锁 10s 后删锁,但此处第一个线程删锁会失败,因为此时删的锁其实是第二个线程的锁,并没有匹配上。
1、如果我们传递了锁的超时时间为 10s,就会发动给 redis 执行脚本,进行占锁,默认超时就是我们指定的超时时间 10s。自己设置锁的超时时间是没有看门狗的,在锁时间到了以后,就不会自动续期。
2、如果我们没有指定锁的超时时间,就使用 30 * 1000ms【LockWatchDogTimeout 看门狗机制的默认时间】;如果占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗机制的默认时间 30s】,每隔 10s 都会自动再次续期,续成 30s。
internalLockLeaseTime【看门狗时间】 / 3 【10s】,10秒之后续期
在实战的时候,推荐使用
lock.lock(30, TimeUnit.SECONDS); //指定一个超时时间,并且省掉了整个续期操作。手动解锁,最慢30秒之后还是会删除锁
@ResponseBody
@RequestMapping("/write")
public String writeValue(){
String s = "";
RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
RLock rLock = lock.writeLock();;
try {
// 1、改数据加写锁,读数据加读锁
rLock.lock();
System.out.println("写锁加锁成功====="+Thread.currentThread().getId());
s = UUID.randomUUID().toString();
Thread.sleep(30000);
redisTemplate.opsForValue().set("writeValue",s);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rLock.unlock();
System.out.println("写锁释放..."+Thread.currentThread().getId());
}
return s;
}
@ResponseBody
@RequestMapping("/read")
public String readValue(){
String s = "";
RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
// 加读锁
RLock rLock = lock.readLock();
rLock.lock();
try {
System.out.println("读锁加锁成功......"+Thread.currentThread().getId());
s = redisTemplate.opsForValue().get("writeValue");
Thread.sleep(30000);
} catch (Exception e) {
e.printStackTrace();
} finally {
rLock.unlock();
System.out.println("读锁释放==="+Thread.currentThread().getId());
}
return s;
}
信号量可以用作分布式限流
获取和释放共用同一个锁 redisson.getSemaphore(“park”)
如果设置 park 的值为10,每获取一次 => 值减一,每释放一次 => 值加一。
acquire():获取一个信号,相当于获取一个值。是一个阻塞方法,如果获取成功,就继续执行,获取失败(值为0,没有信号可以获取了)就一直等待直到获取成功(直到释放一个信号)
tryAcquire():有返回值,会返回 boolean 值,如果有信号就获取,并返回 true,如果没有信号可以获取那就算了,不会阻塞
release():释放一个信号
/**
* 车库停车
* 3个车位
* 信号量可以用作分布式限流,限制每一个应用的流量
*/
@ResponseBody
@GetMapping("/park")
public String park() throws InterruptedException {
RSemaphore park = redisson.getSemaphore("park");
// 占一个车位
park.acquire(); // 获取一个信号,相当于获取一个值。获取成功了。也是一个阻塞方法,获取成功就返回 "ok",获取失败就一直等,直到获取成功
boolean b = park.tryAcquire(); // 有信号我就获取,没有就算了,区别于 acquire 的阻塞(一直等待直到获取)
// if (b) {
// // 执行业务
// } else {
// return "error";
// }
return "ok --- " + b;
}
@ResponseBody
@GetMapping("/go")
public String go() throws InterruptedException {
RSemaphore park = redisson.getSemaphore("park");
// 释放一个车位
park.release(); // 释放一个信号
return "ok";
}
/**
* 放假,锁门
* 等所有班级人走完了,才锁门
*/
@GetMapping("lockDoor")
@ResponseBody
public String lockDoor() throws InterruptedException {
RCountDownLatch door = redisson.getCountDownLatch("door");
door.trySetCount(5);
door.await(); // 等待闭锁都完成
return "放假了...";
}
@GetMapping("/gogogo/{id}")
public String gogogo(@PathVariable("id") String id) {
RCountDownLatch door = redisson.getCountDownLatch("door");
door.countDown(); // 计数减一
return id + "班的人都走了";
}