缓存和分布式锁
缓存的使用
为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。而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(读多写少,即时性,一致性要求不高的数据)来可以使用
- 特殊数据:特殊设计