缓存和分布式锁
缓存的使用
为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。而db承担数据落盘工作。
哪些数据适合放入缓存中?
- 即时性、数据一致性要求不高
- 访问量大且更新评率不高的数据(读多,写少)
data = cache.load(id); // 从缓存中加载数据
if(data == null){
data = db.load(id); // 从数据库中加载数据
cache.put(id,data); // 放入缓存中
}
return data;
使用redis作为缓存
引入jar包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
添加配置文件
高并发下的缓存失效
缓存穿透
指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这个次查询的null写入缓存,这将导致这个不存在的数据每次请求都要去存储层去查询,失去了缓存的意义
风险
利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃
解决
null结果缓存,并加入短暂过期时间
缓存雪崩
指在我们设置缓存时key采用了相同的过去时间,导致缓存在某一时刻同时失效,请求全部转发到db,db瞬时压力过重雪崩
解决
原有的失效时间基础上增加一个随机值
缓存击穿
- 对于一些设置了过期时间的key,如果这些key可能会在某些时间点被高并发的访问,是一种非常“热点”的数据
- 如果这个key在大量请求同时进来前刚好失效,那么对这个key的数据查询都落到db上,就是~
解决
加锁,大量并发只让一个去查,其他人就会等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用到db
使用本地锁,保证查询数据和放入redis缓存中是一个原子性操作,这样可以避免查询2次数据库
分布式加锁
在使用本地锁解决上面的问题的情况下,但是在分布式的情况中,如果启动了多个同一个项目就会查询多次,而不是只查一次。
本地锁只能锁住当前进程,而其他进程仍然会进行查询。
基本原理
使用redis 中的==set key value NX==这个命令,在多个同一个服务往redis中设置值的时候,这个命令的作用是如果存在就不设置了。这样就一次只会有一个服务能够执行。
getDataFromDb()就是最开始的方法,提取出来了。
因为getCatalogJsonFromDbWithLocalLock()和getCatalogJsonFromDbWithRedisLock()都重复了这个方法。
分布式锁演进-阶段一(由于业务原因造成死锁)
问题:
如果占好锁以后由于业务原因宕机了就会造成==死锁==。
解决:
设置锁的自动过期时间,即使没有删除,也会自动删除
分布式锁演进-阶段二(把设置lock和设置过期时间调整为一个原子操作)
分布式锁演进-阶段三
最终-保证获取值进行对比和删除的是一个原子操作
redisson
使用
导入jar包
<!--使用redission作为所有分布式锁,分布式对象功能框架-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.14.0</version>
</dependency>
添加配置类
@Configuration
public class RedissonConfig {
/**
* 所有对redisson的使用都是通过RedissonClient对象
*
* @return {@link RedissonClient}* @throws IOException ioexception
*/
@Bean(destroyMethod = "shutdown")
public RedissonClient redissonClient() throws IOException {
Config config = new Config();
// 1、使用单节点模式
config.useSingleServer()
.setAddress("redis://47.112.150.204:6379");
return Redisson.create(config);
}
}
可重入锁测试
/**
* a、锁会自动续期,默认续期到30s,不用担心业务时间过长,导致锁过期被删除
* b、加锁的业务只要运行完成,就不会续期,当完成后就会在30s内删除
*/
可重入锁测试
基于Redis的Redisson分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。
RLock lock = redisson.getLock("anyLock");
// 最常见的使用方法
lock.lock();
大家都知道,如果负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。
另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。
// 加锁以后10秒钟自动解锁
// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
...
} finally {
lock.unlock();
}
}
Redisson同时还为分布式锁提供了异步执行的相关方法:
RLock lock = redisson.getLock("anyLock");
lock.lockAsync();
lock.lockAsync(10, TimeUnit.SECONDS);
Future<Boolean> res = lock.tryLockAsync(100, 10, TimeUnit.SECONDS);
RLock对象完全符合Java的Lock规范。也就是说只有拥有锁的进程才能解锁,其他进程解锁则会抛出IllegalMonitorStateException错误。但是如果遇到需要其他进程也能解锁的情况,请使用分布式信号量Semaphore 对象.
公平锁
基于Redis的Redisson分布式可重入公平锁也是实现了java.util.concurrent.locks.Lock接口的一种RLock对象。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。它保证了当多个Redisson客户端线程同时请求加锁时,优先分配给先发出请求的线程。所有请求线程会在一个队列中排队,当某个线程出现宕机时,Redisson会等待5秒后继续下一个线程,也就是说如果前面有5个线程都处于等待状态,那么后面的线程会等待至少25秒。
RLock fairLock = redisson.getFairLock("anyLock");
// 最常见的使用方法
fairLock.lock();
读写锁
基于Redis的Redisson分布式可重入读写锁RReadWriteLock Java对象实现了java.util.concurrent.locks.ReadWriteLock接口。其中读锁和写锁都继承了RLock接口。
分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。
RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");
// 最常见的使用方法
rwlock.readLock().lock();
// 或
rwlock.writeLock().lock();
大家都知道,如果负责储存这个分布式锁的Redis节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。
另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。
// 10秒钟以后自动解锁
// 无需调用unlock方法手动解锁
rwlock.readLock().lock(10, TimeUnit.SECONDS);
// 或
rwlock.writeLock().lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = rwlock.readLock().tryLock(100, 10, TimeUnit.SECONDS);
// 或
boolean res = rwlock.writeLock().tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();
闭锁(CountDownLatch)
当 gogogo这个方法被调用5次后,lockDoor运行
信号量
总共只有
基于Redis的Redisson的分布式信号量(Semaphore)Java对象RSemaphore采用了与java.util.concurrent.Semaphore相似的接口和用法。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。
RSemaphore semaphore = redisson.getSemaphore("semaphore");
semaphore.acquire();
//或
semaphore.acquireAsync();
semaphore.acquire(23);
semaphore.tryAcquire();
//或
semaphore.tryAcquireAsync();
semaphore.tryAcquire(23, TimeUnit.SECONDS);
//或
semaphore.tryAcquireAsync(23, TimeUnit.SECONDS);
semaphore.release(10);
semaphore.release();
//或
semaphore.releaseAsync();
使用redisson分布式加锁
redis缓存数据一致性
1.双写模式 2.失效模式
spring cache
使用
依赖
<!--使用spring cache-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置
CacheAutoConfiguration
RedisCacheConfiguration RedisCacheManager主要配置都在CacheProperties中
spring:
cache:
type: redis
使用
第一次
第二次,没有调用这个方法
常用注解
@EnableCaching 在配置类上添加配置,开启缓存
-
@Cacheable: Triggers cache population.- 触发将会把数据保存到缓存中
-
@CacheEvict: Triggers cache eviction.- 触发将会把数据从缓存中删除
-
@CachePut: Updates the cache without interfering with the method execution.- 不影响方法执行,更新缓存
-
@Caching: Regroups multiple cache operations to be applied on a method.-
组合以上多个操作
@Caching(cacheable = { @Cacheable(value = {"cache1"},key = "#root.method.name"), @Cacheable(value = {"cache2"},key = "#root.method.name") }, evict = { @CacheEvict(value = {"evict1"},key ="#root.method.name" ) } )
-
-
@CacheConfig: Shares some common cache-related settings at class-level.- 在类级别共享缓存的相同配置
默认行为
- 如果缓存命中,方法不掉用
- ==key默认自动生成,缓存指定的value::simpleKey[]自动生成的key值==
- ==缓存的value的值,默认实现jdk序列化机制,将序列化后的数据存到redis==
- ==过期时间TTL是-1,永不过期==
自定义
-
指定缓存使用的key
-
指定缓存的ttl时间
可以使用spel表达式
-
指定缓存数据为json格式
抽取配置类
@Configuration
@EnableCaching
@EnableConfigurationProperties(CacheProperties.class) // 读取配置文件中的配置
public class CacheConfig {
/**
* 配置文件中的配置没有用上
*
* @return {@link RedisCacheConfiguration}
*/
@Bean
public RedisCacheConfiguration redisCacheConfiguration() {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
// 序列化机制:使用json格式缓存
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.json()));
// 设置ttl时间
config = config.entryTtl(Duration.ofDays(1));
// 默认缓存空值,设置为不缓存空值
// config = config.disableCachingNullValues();
return config;
}
}
不足
读模式
-
缓存穿透:
查询一个null数据,解决:缓存空数据, config = config.disableCachingNullValues(); -
缓存击穿
大量并发进来同时查一个数据,且这个数据刚好失效:解决加锁 默认是没有加锁的 -
缓存雪崩
大量的key同时过期,解决:加随机时间(直接指定过期时间即可)
写模式
缓存一致性
- 读写的加锁,有序进行(读多写少的系统)
- 引入canal,感知到mysql的更新操作
- 读多写少,直接去数据库中查询就可以
总结
- 常规数据可以使用spring cache(读多写少,即时性,一致性要求不高的数据)来可以使用
- 特殊数据:特殊设计