哪些数据适合放入缓存?
即时性、数据一致性要求不高的
访问量大且更新频率不高的数据(读多,写少)
凡是放入缓存中的数据我们都应该指定过期时间,使其可以在系统即使没
有主动更新数据也能自动触发数据加载进缓存的流程。避免业务崩溃导致的数据永久不一致
问题。
<!-- 引入redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
redis:
host: 127.0.0.1
password: mima
port: 6379
@Test
public void testStringRedisTemplate(){
//往Redis中存入key
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
//保存数据
ops.set("hello", "word_"+ UUID.randomUUID().toString());
//查询
String hello = ops.get("hello");
System.out.println("redis:"+hello);
}
线程数:50
出现对外内存溢出异常
Redis exception; nested exception is io.lettuce.core.RedisException:
io.netty.util.internal.OutOfDirectMemoryError: failed to allocate
46137344 byte(s) of direct memory (used: 58720256, max: 100663296)
原因
-Dio.netty.maxDirectMemory
进行设置解决:
不能只去使用 -Dio.netty.maxDirectMemory 调大堆外内存
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
<exclusions>
<exclusion>
<groupId>io.lettucegroupId>
<artifactId>lettuce-coreartifactId>
exclusion>
exclusions>
dependency>
引入jedis
版本由springboot控制
<jedis.version>2.9.3jedis.version>
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
dependency>
lettuce 和 jedis的区别
都是操作redis最低层的客户端,spring会将他俩再次封装成 redisTemplate,所以可以更换为jedis
在SpringData的底层配置文件RedisAutoConfiguration中对此进行了配置
缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数
据库也无此记录,我们没有将这次查询的null 写入缓存,这将导致这个不存在的数据每次 请求都要到存储层去查询,失去了缓存的意义。
在流量大时,可能DB 就挂掉了,要是有人利用不存在的key 频繁攻击我们的应用,这就是 漏洞。
解决:
缓存查询的null、并且设置短的过期时间。
缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失
效,请求全部转发到DB,DB 瞬时压力过重雪崩。
解决:
原有的失效时间基础上增加一个随机值,比如1-5 分钟随机,这样每一个缓存的过期时间的 重复率就会降低,就很难引发集体失效的事件。
对于一些设置了过期时间的key,如果这些key 可能会在某些时间点被超高并发地访问,
是一种非常“热点”的数据。
这个时候,需要考虑一个问题:如果这个key 在大量请求同时进来前正好失效,那么所
有对这个key 的数据查询都落到db,我们称为缓存击穿。
解决:
加锁,大量并发只让一个人去查,查到以后释放锁,其他的人获取到锁,先查缓存就会有数据,不用去db
加锁方式:将代码放入同步代码块
只要是同一把锁,就能锁住,需要这个锁的所有线程
1.使用this当前对象加锁,SpringBoot所有的组件在容器中都是单例的,相当于有多少请求都会用同一个this,是可以的
synchronized (this){
//得到锁以后应该再去缓存中确定一次,如果没有才需要继续查询
String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
//如果不是空的直接返回
if (!StringUtils.isEmpty(catalogJSON)){
//缓存不为空直接返回
Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>(){});
return result;
}
//执行查询数据库
......
}
本地锁synchronized,JUC包下的(lock)只能锁当前进程,在分布式情况下想要锁住全部必须使用分布式锁,分布式锁相比本地锁性能会有所差距
测试 本地锁在分布式情况下会产生什么问题?
在用过gateway负载均衡路由到服务上时
每个服务都会有一次查询请求
1.使用redis的SET key value [EX seconds] [PX milliseconds] [NX|XX]
是一种用 Redis 来实现锁机制的简单方法
EX seconds – 设置键key的过期时间,单位时秒
PX milliseconds – 设置键key的过期时间,单位时毫秒
NX – 只有键key不存在的时候才会设置key的值
XX – 只有键key存在的时候才会设置key的值
如果上述命令返回OK,那么客户端就可以获得锁(如果上述命令返回Nil,那么客户端可以在一段时间之后重新尝试),并且可以通过DEL命令来释放锁。
客户端加锁之后,如果没有主动释放,会在过期时间之后自动释放。
1. 如果在执行业务代码之后没有删除锁怎么办?
给锁设置超时时间,就算代码没有删除锁,redis也会自动删除锁
//1.占分布式锁 去redis占坑
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");
if (lock) {
//加锁成功....执行业务
//2.设置过期时间,到期自动删除锁
redisTemplate.expire("lock", 30, TimeUnit.SECONDS);
Map<String, List<Catelog2Vo>> dataFromDB = getDataFromDB();
//删除锁
redisTemplate.delete("lock");
return dataFromDB;
} else {
//加锁失败...重试...一直重试 称之为自旋锁
//休眠100ms进行重试
return getCatalogJsonFromDbWithRedisLock();
}
2.如果抢占锁成功了,但是由于各种原因没有成功设置超时时间,造成死锁
如果占锁和设置超时时间是一个原子操作,占锁的同时加上过期时间EX seconds – 设置键key的过期时间,单位时秒
//1.占分布式锁 去redis占坑 并设置过期时间,到期自动删除锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111",300, TimeUnit.SECONDS);
if (lock) {
//加锁成功....执行业务
Map<String, List<Catelog2Vo>> dataFromDB = getDataFromDB();
//删除锁
redisTemplate.delete("lock");
return dataFromDB;
} else {
//加锁失败...重试...一直重试 称之为自旋锁
//休眠100ms进行重试
return getCatalogJsonFromDbWithRedisLock();
}
3. 在设置完过期时间后,如果执行业务代码时间过长,再去删锁
锁因为超时时间已经删除,可能就会去删一个不存在的锁。
假如说第一个线程在执行到10秒的时候自己锁已经过期了,这时候第二个线程又抢占了这个锁再去执行业务代码,而此时第一个线程的业务代码执行完毕,把第二个线程正在使用的锁给删除了
指定值为uuid 删锁的时候匹配成功才去删
//1.占分布式锁 去redis占坑 并设置过期时间,到期自动删除锁
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,300, TimeUnit.SECONDS);
if (lock) {
//加锁成功....执行业务
Map<String, List<Catelog2Vo>> dataFromDB = getDataFromDB();
//先去查一下
String lockValue = redisTemplate.opsForValue().get("lock");
//如果值相同
if (uuid.equals(lockValue)){
//删除自己的锁
redisTemplate.delete("lock");
}
return dataFromDB;
} else {
//加锁失败...重试...一直重试 称之为自旋锁
//休眠100ms进行重试
return getCatalogJsonFromDbWithRedisLock();
}
4. 如果在判断uuid是否为当前锁的时候,锁已经过期,这时候别的线程已经设置了新的值,这时候删除的是别人的锁,获取值对比+值相同删除=原子操作
使用 lua 脚本解锁
//1.占分布式锁 去redis占坑 并设置过期时间,到期自动删除锁
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
if (lock) {
System.out.println("获取分布式锁成功");
Map<String, List<Catelog2Vo>> dataFromDB;
try{
//加锁成功....执行业务
dataFromDB = getDataFromDB();
}finally {
//获取值对比+值相同删除=原子操作
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
//删除锁
Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
}
return dataFromDB;
} else {
//加锁失败...重试...一直重试 称之为自旋锁
//休眠100ms进行重试
System.out.println("获取分布式锁失败....等待重试");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCatalogJsonFromDbWithRedisLock();
}
保证加锁【占位+过期时间】和删除锁【判断+删除】的原子性。
Redisson 是架设在Redis 基础上的一个Java 驻内存数据网格(In-Memory Data Grid)。充分
的利用了Redis 键值数据库提供的一系列优势,基于Java 实用工具包中常用接口,为使用者
提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工
具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式
系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间
的协作。
<!-- 以后使用redisson作为分布式锁 分布式对象的框架 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>
程序化配置方法
Redisson程序化的配置方法是通过构建Config对象实例来实现
package cn.cloud.xmall.product.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
/**
* @Description: Redisson配置类
* @author: Freedom
* @QQ: 1556507698
* @date:2022/3/16 18:35
*/
@Configuration
public class MyRedissonConfig {
/**
* 所有对Redisson的使用都是通过RedissonClient
* @return
* @throws IOException
*/
@Bean(destroyMethod="shutdown")
public RedissonClient redisson() throws IOException {
//1、创建配置
Config config = new Config();
//指定使用单节点配置
config.useSingleServer().setAddress("redis://101.43.122.84:6379").setPassword("YourPassword");
//2、根据Config创建出RedissonClient实例
//Redis url should start with redis:// or rediss://
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
基于Redis的Redisson分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.lock
的接口
可重入锁
例如A,B两个方法,A方法加了一号锁,在A方法的内部调用B方法,此时B方法也想加一号锁,B方法发现一号锁在A方法上,就可以直接执行,这就叫可重入锁,所有的锁都应该设计成可重入锁
myLock.lock(10,TimeUnit.SECONDS);
//10秒钟自动解锁,自动解锁时间一定要大于业务执行时间
问题:在锁时间到了以后,不会自动续期
@ResponseBody
@GetMapping("/hello")
public String hello(){
//1.获取一把锁,只要锁名字相同就是同一把锁
RLock lock = redisson.getLock("my-lock");
//2.加锁 也可以指定时间
lock.lock(); //阻塞式等待 加不到锁就会一直等
//1.如果我们
try{
System.out.println("加锁成功..执行业务...."+Thread.currentThread().getId());
Thread.sleep(30000);
}catch (Exception e){
}finally {
//解锁
lock.unlock();
System.out.println("释放锁"+Thread.currentThread().getId());
}
return "hello";
};
**Redisson解决了 锁的自动续期问题,如果业务时间超常 他会自动给锁续上新的30秒周期,不用担心业务时间长 锁自动过期被删掉,默认加的锁都是30秒
加锁的业务只要运行完成,就不会给当前的锁续期,即使不手动解锁,锁也会默认在30秒以后自动删除
如果手动设置锁的超时时间的话,超时时间一定要大于业务的执行时间
@Override
public void lock(long leaseTime, TimeUnit unit) {
try {
lock(leaseTime, unit, false);
} catch (InterruptedException e) {
throw new IllegalStateException();
}
}
@Override
public void lock() {
try {
lock(-1, null, false);
} catch (InterruptedException e) {
throw new IllegalStateException();
}
}
2.默认默认指定超时时间又调用了
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
//获取线程id
long threadId = Thread.currentThread().getId();
//尝试来获取
Long ttl = tryAcquire(leaseTime, unit, threadId);
// 如果尝试获取返回null 那么会认为锁获取到了
if (ttl == null) {
//直接返回
return;
}
RFuture<RedissonLockEntry> future = subscribe(threadId);
if (interruptibly) {
commandExecutor.syncSubscriptionInterrupted(future);
} else {
commandExecutor.syncSubscription(future);
}
try {
//获取不到锁会调用这个死循环一直获取
while (true) {
ttl = tryAcquire(leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
//直到获取到锁
break;
}
// waiting for message
if (ttl >= 0) {
try {
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
if (interruptibly) {
throw e;
}
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
}
} else {
if (interruptibly) {
future.getNow().getLatch().acquire();
} else {
future.getNow().getLatch().acquireUninterruptibly();
}
}
}
} finally {
unsubscribe(future, threadId);
}
// get(lockAsync(leaseTime, unit));
}
tryAcquire(long leaseTime, TimeUnit unit, long threadId) 上一个方法调用的获取锁的方法
private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
return get(tryAcquireAsync(leaseTime, unit, threadId));
}
// leaseTime 我们传入的超时时间
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
//如果我们传入了超时时间 即 不等于 -1 注意:如果不传入超时时间的话就是-1
if (leaseTime != -1) {
//尝试使用异步方式进行加锁
return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}
RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e != null) {
return;
}
// lock acquired
if (ttlRemaining == null) {
scheduleExpirationRenewal(threadId);
}
});
return ttlRemainingFuture;
}
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
//先将我们传入的时间转换为内部锁的释放时间
internalLockLeaseTime = unit.toMillis(leaseTime);
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"return redis.call('pttl', KEYS[1]);",
Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
总结
1. 如果我们传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是 我们制定的时间
2. 如果我们指定锁的超时时间,就使用 lockWatchdogTimeout = 30 * 1000 【看门狗默认时间】
只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10秒都会自动的再次续期,续成30秒
3. internalLockLeaseTime 【看门狗时间】 / 3, 10s
最佳实践使用指定超时时间的加锁方法,这样还省掉了续期时间
写数据加写锁
读数据加读锁
@GetMapping(value = "/write")
@ResponseBody
public String writeValue() {
String s = "";
RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
RLock rLock = readWriteLock.writeLock();
try {
//1、改数据加写锁,读数据加读锁
rLock.lock();
s = UUID.randomUUID().toString();
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
ops.set("writeValue",s);
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rLock.unlock();
}
return s;
}
@GetMapping(value = "/read")
@ResponseBody
public String readValue() {
String s = "";
RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
//加读锁
RLock rLock = readWriteLock.readLock();
try {
rLock.lock();
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
s = ops.get("writeValue");
try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
} catch (Exception e) {
e.printStackTrace();
} finally {
rLock.unlock();
}
return s;
}
可以用来做限流
/**
* 车库停车,走了一个,停一个车
* 3车位
* 信号量也可以做分布式限流
*/
@GetMapping(value = "/park")
@ResponseBody
public String park() throws InterruptedException { //停车请求
RSemaphore park = redisson.getSemaphore("park");
park.acquire(); //获取一个信号、获取一个值,占一个车位 阻塞方法
boolean flag = park.tryAcquire(); //尝试获取
if (flag) {
//执行业务
} else {
return "error";
}
return "ok=>" + flag;
}
@GetMapping(value = "/go")
@ResponseBody
public String go() {
RSemaphore park = redisson.getSemaphore("park");
park.release(); //释放一个车位
return "ok";
}
/**
* 放假、锁门
* 1班没人了
* 5个班,全部走完,我们才可以锁大门 即redis中存的5变成0
* 分布式闭锁
*/
@GetMapping(value = "/lockDoor")
@ResponseBody
public String lockDoor() throws InterruptedException {
RCountDownLatch door = redisson.getCountDownLatch("door");
door.trySetCount(5); //等待5个班的人
door.await(); //等待闭锁完成
return "放假了...";
}
@GetMapping(value = "/gogogo/{id}")
@ResponseBody
public String gogogo(@PathVariable("id") Long id) {
RCountDownLatch door = redisson.getCountDownLatch("door");
door.countDown(); //计数-1
return id + "班的人都走了...";
}
缓存中的数据 如何和数据库保持一致?
双写模式
在更新数据库中的数据时,要同时修改缓存中的数据,但是可能会出现短时间的数据不一致
失效模式
在修改完数据库中的数据后 ,删除掉缓存中的数据,下次再查询就会主动查询数据库更新,但是在有些情况下还是会出现脏数据问题,注意 如果是需要经常修改,经常查询的数据,应该直接读数据库,可以考虑加读写锁,使用到缓存一般都是读多写少,所以用读写锁比较好
好处就是在编码期间只考虑修改数据库,Cannl在后台自己改,缺点就是增加中间件,
Cannl还可以重组我们不同架构的数据