哪些数据适合放入缓存:
- 即时性、数据一致性要求不高
- 访问量大且更新频率不高(读多,写少)
读模式缓存使用流程
docker环境下使用redis
# 拉取redis
docker pull redis:6.0.9
# 启动redis并设置密码为123456
docker run --name redis -p 6379:6379 -d redis:6.0.9 --requirepass "123456"
# 设置开机自启
docker update redis --restart=always
使用redis作为缓存
spring boot引入redis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置redis连接信息
spring:
redis:
host: 192.168.56.10
port: 6379
password: test
进行压测产生堆外内存溢出:OutOfDirectMemoryError
产生原因:
springboot2.0以后默认使用lettuce作为操作redis的客户端,它使用netty进行网络通信。lettuce的bug导致netty的堆外内存溢出(netty如果没有指定堆外内存,默认使用-Xmx100m)
解决方案(不能使用-Dio.netty.maxDirectMemory只去调整堆外内存):
- 升级lettuce的客户端
- 使用jedis
<!-- 切换客户端为jedis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
高并发下缓存失效问题:
缓存穿透:
指查询一个一定不存在的数据,由于缓存不命中,将去查询数据库,但是数据库也无此纪录,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层查询,失去了缓存的意义
风险:
利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃
解决:
null结果缓存,并加入短暂过期时间
缓存雪崩:
缓存雪崩是指在我们设置缓存时key采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
解决:
原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
缓存击穿:
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常"热点"的数据。如果这个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。
解决:
加锁。大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去db
加锁:
只要是同一把锁,就能锁住需要这个锁的所有线程。
- 本地锁:synchronized(this)、JUC(Lock),springboot所有组件在容器中都是单例的,只能锁住当前进程
- 分布式锁:由于在分布式情况下有多个容器,所以想要锁住所有,必须使用分布式锁
// 本地锁
private Map<Long,List<Category2Level>> getCategorysJsonFromDB() {
synchronized (this){
// 当其他的线程进来的时候直接查询缓存,避免操作数据库
String s = redisTemplate.opsForValue().get(CATEGORY_JSON_KEY);
// 缓存中没有
if (StringUtils.isEmpty(s)){
// System.out.println("查询数据库"); getDatasFromDB();是业务逻辑
Map<Long, List<Category2Level>> datasFromDB = getDatasFromDB();
// 在redis缓存中存数据 在锁结束时保存到缓存中
redisTemplate.opsForValue().set(KEY,VALUE,1, TimeUnit.DAYS);
return datasFromDB;
}
Map<Long, List<Category2Level>> longListMap = JSON.parseObject(s, new TypeReference<Map<Long, List<Category2Level>>>() {});
return longListMap;
}
}
分布式锁的演进阶段:
// 伪代码
public void getData(){
Boolean lock = redisTemplate.opsForValue.setUfAbsent("lock","1111");
if(lock){
// 执行业务..
// 删除锁
redisTemplate.delete("lock");
} else{
// 继续调用getData,等待锁的释放,在此期间可以休眠一段时间
getData();
}
}
// 命令:SET key value [EX seconds] [PX milliseconds] [NX'XX]
// EX seconds 设置键key的过期时间,单位时秒
// PX milliseconds 设置键key的过期时间,单位时毫秒
// NX 只有键key不存在的时候才会设置key的值
// XX 只有键key存在的时候才会设置key的值
// 伪代码
public void getData(){
// 设置了过期时间 - 180s
Boolean lock = redisTemplate.opsForValue.setUfAbsent("lock","1111",180,TimeUnit.SECONDS);
if(lock){
// 执行业务...
// 删除锁
redisTemplate.delete("lock");
} else{
// 继续调用getData,等待锁的释放,在此期间可以休眠一段时间
getData();
}
}
// 伪代码
public void getData(){
// 使用UUID生成不重复值
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue.setUfAbsent("lock",uuid,180,TimeUnit.SECONDS);
if(lock){
// 执行业务...
// 判断uuid是否相同
String str = redisTemplate.get("lock");
if(uuid.equals(str){
// 相同,就删除锁
redisTemplate.delete("lock");
}
} else{
// 继续调用getData,等待锁的释放,在此期间可以休眠一段时间
getData();
}
}
public void getData(){
// 使用UUID生成不重复值
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue.setUfAbsent("lock",uuid,180,TimeUnit.SECONDS);
if(lock){
try{
// 执行业务...
}
finally{
// Lua脚本 - 2个参数 key和value
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
long result = redisTemplate.execute(new DefaultRedisScript<long>(script,long.class),Arrays.asList("lock"),uuid);
}
} else{
// 继续调用getData,等待锁的释放,在此期间可以休眠一段时间
getData();
}
}
整合redisson作为分布式锁、分布式对象等功能框架
1、引入依赖
<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.5</version>
</dependency>
2、redisson配置(参考:https://github.com/redisson/redisson/wiki/14.-%E7%AC%AC%E4%B8%89%E6%96%B9%E6%A1%86%E6%9E%B6%E6%95%B4%E5%90%88#142-spring-cache%E6%95%B4%E5%90%88)
@Bean(destroyMethod="shutdown")
RedissonClient redisson() throws IOException {
Config config = new Config();
// 地址前必须添加"redis://",不然启动会报错
// 可以用"rediss://"来启用ssl连接
config.useClusterServers().addNodeAddress("redis://127.0.0.1:7004", "redis://127.0.0.1:7001");
return Redisson.create(config);
}
// 修改为单节点模式(参考:https://github.com/redisson/redisson/wiki/2.-%E9%85%8D%E7%BD%AE%E6%96%B9%E6%B3%95#26-%E5%8D%95redis%E8%8A%82%E7%82%B9%E6%A8%A1%E5%BC%8F):
// 默认连接地址 127.0.0.1:6379
RedissonClient redisson = Redisson.create();
Config config = new Config();
config.useSingleServer().setAddress("redis://myredisserver:6379");
RedissonClient redisson = Redisson.create(config);
3、分布式锁
可重用锁(Reentrant Lock)(https://github.com/redisson/redisson/wiki/8.-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E5%92%8C%E5%90%8C%E6%AD%A5%E5%99%A8)
// 加锁
RLock lock = redissonClient.getLock("hello");
// 阻塞式等待,默认加锁都是30s
// lock.lock();
// 优势:
// 1、锁的自动续期,如果业务时间超长,运行期间自动给锁续上新的30s,不用担心业务时间长,锁过期被自动删掉
// 2、加锁的业务只要运行完成,就不会给当前锁自动续期,即使不手动解锁,锁默认在30s后自动删除
// 加锁以后10秒钟自动解锁,自动解锁时间必须要大于业务时间
// 无需调用unlock方法手动解锁
// 1、如果传递了锁的超时时间,就会发送给redis执行脚本,进行占锁,默认超时时间就是指定的时间
// 2、如果未指定超时时间,就会使用30*1000【lookWatchdongTimeout看门狗的默认时间】
// 只要占锁成功,就会启动一个定时任务【重新设置过期时间,新的过期时间就是看门狗的默认时间】
// 每隔internalLockLeaseTime【看门狗时间】/ 3自动续30s
lock.lock(10, TimeUnit.SECONDS);
//省掉了整个续期时间
try {
System.out.println("加锁成功" + Thread.currentThread().getId());
Thread.sleep(3000);
} catch (Exception e) {
System.out.print(e);
} finally {
System.out.println("释放锁" + Thread.currentThread().getId());
lock.unlock();
}
读写锁(ReadWriteLock)(含有写的过程都会被阻塞,只有读读不会被阻塞)
@GetMapping("/read")
@ResponseBody
public String read() {
RReadWriteLock lock = redissonClient.getReadWriteLock("ReadWrite-Lock");
RLock rLock = lock.readLock();
String s = "";
try {
rLock.lock();
System.out.println("读锁加锁"+Thread.currentThread().getId());
Thread.sleep(5000);
s= redisTemplate.opsForValue().get("lock-value");
}
finally {
rLock.unlock();
return "读取完成:"+s;
}
}
@GetMapping("/write")
@ResponseBody
public String write() {
RReadWriteLock lock = redissonClient.getReadWriteLock("ReadWrite-Lock");
RLock wLock = lock.writeLock();
String s = UUID.randomUUID().toString();
try {
wLock.lock();
System.out.println("写锁加锁"+Thread.currentThread().getId());
Thread.sleep(10000);
redisTemplate.opsForValue().set("lock-value",s);
}
catch (InterruptedException e) {
e.printStackTrace();
}
finally {
wLock.unlock();
return "写入完成:"+s;
}
}
信号量(Semaphore):
信号量为存储在redis中的一个数字,当这个数字大于0时,即可以调用acquire()
方法增加数量,也可以调用release()
方法减少数量,但是当调用release()
之后小于0的话方法就会阻塞,直到数字大于0
@GetMapping("/park")
@ResponseBody
public String park() {
RSemaphore park = redissonClient.getSemaphore("park");
try {
park.acquire(2);
}
catch (InterruptedException e) {
e.printStackTrace();
}
return "停进2";
}
@GetMapping("/go")
@ResponseBody
public String go() {
RSemaphore park = redissonClient.getSemaphore("park");
park.release(2);
return "开走2";
}
闭锁(CountDownLatch):
可以理解为门栓,使用若干个门栓将当前方法阻塞,只有当全部门栓都被放开时,当前方法才能继续执行。
以下代码只有offLatch()
被调用5次后 setLatch()
才能继续执行
@GetMapping("/setLatch")
@ResponseBody
public String setLatch() {
RCountDownLatch latch = redissonClient.getCountDownLatch("CountDownLatch");
try {
latch.trySetCount(5);
latch.await();
}
catch (InterruptedException e) {
e.printStackTrace();
}
return "门栓被放开";
}
@GetMapping("/offLatch")
@ResponseBody
public String offLatch() {
RCountDownLatch latch = redissonClient.getCountDownLatch("CountDownLatch");
latch.countDown();
return "门栓被放开1";
}
缓存一致性