微服务进阶学习整合高级篇--缓存
缓存的使用选择条件
- 即时性、数据一致性要求不高的
- 读的频率高,修改的频率少的
redis基本使用
缓存中间件redis入门使用
- 引入redis坐标
#这里注意springboot版本,springboot稍微旧一点的版本不需要加上-data-字样
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- yml配置redis相关信息
- 注入需要使用的redistemplate,也可以根据自己的需求配置对应的redisTemplate。可以在自定义redisTemplate中进行序列化与反序列化,也可以在代码中每次书写的时候手动序列化
@Override
public Map<String, List<Catelog2Vo>> getCatalogJson2(){
//先去redis中查询缓存是否存在需要读的数据
String catalogJson = redisTemplate.opsForValue().get("catalogJson");
//如果读不到缓存,缓存不存在
if(StringUtils.isEmpty(catalogJson)){
//去db读取缓存
Map<String, List<Catelog2Vo>> catalogJson2FromDB = this.getCatalogJson2FromDB();
//将查询的数据序列化成json对象,设置缓存方便下次使用
String cacheCatalog = JSON.toJSONString(catalogJson2FromDB);
redisTemplate.opsForValue().set("catalogJson",cacheCatalog);
return catalogJson2FromDB;
}
//如果查到缓存,则读取缓存,将缓存的json转换成对象,使用匿名内部类将json转换成指定的类型。
Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>(){});
return result;
}
压力测试下的堆外溢出
- 使用jmeter对上了缓存的接口进行压力测试,一开始还不会出现错误,随着时间的推移,开始出现堆外内存溢出的情况,如:
io.netty.util.internal.OutOfDirectMemoryError: failed to allocate 46137344 byte(s) of direct memory (used: 58720256, max: 100663296)
at io.netty.util.internal.PlatformDependent.incrementMemoryCounter(PlatformDependent.java:725) ~[netty-common-4.1.39.Final.jar:4.1.39.Final]
at io.netty.util.internal.PlatformDependent.allocateDirectNoCleaner(PlatformDependent.java:680) ~[netty-common-4.1.39.Final.jar:4.1.39.Final]
at io.netty.buffer.PoolArena$DirectArena.allocateDirect(PoolArena.java:772) ~[netty-buffer-4.1.39.Final.jar:4.1.39.Final]
at io.netty.buffer.PoolArena$DirectArena.newUnpooledChunk(PoolArena.java:762) ~[netty-buffer-4.1.39.Final.jar:4.1.39.Final]
at io.netty.buffer.PoolArena.allocateHuge(PoolArena.java:260) ~[netty-buffer-4.1.39.Final.jar:4.1.39.Final]
at io.netty.buffer.PoolArena.allocate(PoolArena.java:232) ~[netty-buffer-4.1.39.Final.jar:4.1.39.Final]
at io.netty.buffer.PoolArena.reallocate(PoolArena.java:400) ~[netty-buffer-4.1.39.Final.jar:4.1.39.Final]
at io.netty.buffer.PooledByteBuf.capacity(PooledByteBuf.java:119) ~[netty-buffer-4.1.39.Final.jar:4.1.39.Final]
at io.netty.buffer.AbstractByteBuf.ensureWritable0(AbstractByteBuf.java:303) ~[netty-buffer-4.1.39.Final.jar:4.1.39.Final]
at io.netty.buffer.AbstractByteBuf.ensureWritable(AbstractByteBuf.java:274) ~[netty-buffer-4.1.39.Final.jar:4.1.39.Final]
at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1111) ~[netty-buffer-4.1.39.Final.jar:4.1.39.Final]
at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1104) ~[netty-buffer-4.1.39.Final.jar:4.1.39.Final]
at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1095) ~[netty-buffer-4.1.39.Final.jar:4.1.39.Final]
at io.lettuce.core.protocol.CommandHandler.channelRead(CommandHandler.java:554) ~[lettuce-core-5.1.8.RELEASE.jar:na]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:374) [netty-transport-4.1.39.Final.jar:4.1.39.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:360) [netty-transport-4.1.39.Final.jar:4.1.39.Final]
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:352) [netty-transport-4.1.39.Final.jar:4.1.39.Final]
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1421) [netty-transport-4.1.39.Final.jar:4.1.39.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:374) [netty-transport-4.1.39.Final.jar:4.1.39.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:360) [netty-transport-4.1.39.Final.jar:4.1.39.Final]
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:930) [netty-transport-4.1.39.Final.jar:4.1.39.Final]
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163) [netty-transport-4.1.39.Final.jar:4.1.39.Final]
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:697) [netty-transport-4.1.39.Final.jar:4.1.39.Final]
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:632) [netty-transport-4.1.39.Final.jar:4.1.39.Final]
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:549) [netty-transport-4.1.39.Final.jar:4.1.39.Final]
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:511) [netty-transport-4.1.39.Final.jar:4.1.39.Final]
at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:918) [netty-common-4.1.39.Final.jar:4.1.39.Final]
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) [netty-common-4.1.39.Final.jar:4.1.39.Final]
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) [netty-common-4.1.39.Final.jar:4.1.39.Final]
at java.lang.Thread.run(Thread.java:748) [na:1.8.0_291]
2021-06-18 10:54:17.263 WARN 9796 --- [ioEventLoop-4-2] io.lettuce.core.protocol.CommandHandler : null Unexpected exception during request: io.netty.util.internal.OutOfDirectMemoryError: failed to allocate 46137344 byte(s) of direct memory (used: 58720256, max: 100663296)
- 出现该问题的原因是因为springboot2.0后采用的是lettuce客户端对redis进行操作,lettuce底层使用的是netty,netty如果不指定堆外内存,那他使用的就是我们为项目定义的-Xms 的内存。而netty使用的内存不能够及时释放,从而造成对外内存溢出
- 解决方案:可以在选择springboot版本的时候,选择高版本的springboot,如2.3.x(本质上也是升级lettuce客户端);也可以改用jedis客户端进行redis操作;
缓存的使用
@Override
public Map<String, List<Catelog2Vo>> getCatalogJson2(){
//先去redis中查询缓存是否存在需要读的数据
String catalogJson = redisTemplate.opsForValue().get("catalogJson");
//如果读不到缓存,缓存不存在
if(StringUtils.isEmpty(catalogJson)){
System.out.println("缓存不命中,查询数据库");
//去db读取缓存
Map<String, List<Catelog2Vo>> catalogJson2FromDB = this.getCatalogJson2FromDB();
return catalogJson2FromDB;
}
System.out.println("缓存命中,没有查询数据库");
//如果查到缓存,则读取缓存,将缓存的json转换成对象,使用匿名内部类。在这里也可以通过自己配置的redisTemplate,在配置文件中统一对数据进行序列化与反序列化处理
Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>(){});
return result;
}
public Map<String, List<Catelog2Vo>> getCatalogJson2FromDB() {
/**
* 本地锁的方式锁住进程,只允许一个进程进来查看数据,防止高并发下缓存击穿的问题。默认单例模式因此只会存在一个getCatalogJson2FromDB资源
* 双重校验锁,确保进来的线程不会重复查询数据库
* 分布式情况下本地锁方式还是会存在问题,假如说日后服务是集群的形式,有8台product服务,每一台服务都会只会锁住当前服务的当前线程,还是会存在多个服务访问统一资源
* */
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>>>(){});
System.out.println("缓存命中,没有重复查询数据库");
return result;
}
/**
* 业务逻辑代码
* */
//将查询的数据序列化成json对象,设置缓存方便下次使用,将缓存设置进redis的操作放进锁里面,避免时序性带来的重复查询db的问题
String cacheCatalog = JSON.toJSONString(map);
redisTemplate.opsForValue().set("catalogJson",cacheCatalog);
return map;
}
}
分布式锁Redisson-lock
- 其原理其实还是通过
setnx
实现的。setnx的意思是不存在即创建,存在的话则不创建。即同一时刻只能设置成功一个。 - springboot整合redisson
TODO
- 非springboot整合redisson
1、导入maven坐标
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>
2、编写配置文件
@Configuration
public class MyRedisConfig {
// destoryMethod为redisson被销毁时调用的方法。
@Bean(destroyMethod = "shutdown")
public RedissonClient redisson() {
Config config = new Config();
// 创建单例模式的配置
config.useSingleServer().setAddress("redis://" + YourIP + ":6379");
return Redisson.create(config);
}
}
3、测试
@ResponseBody
@GetMapping("/hello")
private String hello(){
//注意这里,这里只要调用的锁名字相同,那么就是同一把锁。两个商品服务都调用了这个锁,那么只有一个会被锁住。
//很好的解决了分布式下缓存不一致的问题
//RLock本质上其实是可重入锁
//阻塞等待机制(默认),可以自己设置等待时间以及上锁后自动解锁时间
RLock lock = redisson.getLock("ny_lock");
// 自动解锁,加锁以后10秒钟自动解锁,看门狗不续命,使用的话自动解锁时间必须大于业务时间。
//lock.lock(10, TimeUnit.SECONDS);
//上锁
lock.lock();
try{
System.out.println(Thread.currentThread().getId()+":"+Thread.currentThread().getName()+"加锁成功");
Thread.sleep(10000);
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getId()+":"+Thread.currentThread().getName()+"解锁成功");
lock.unlock();
}
return "hello";
}
#
看门狗机制可以确保在redission在关闭之前,即该线程执行完之前,
或异常退出之前,该线程一直占有该线程需要的锁,锁续期时间为30秒。
很好的解决了如某线程占有锁期间服务宕机导致死锁的情况,以及某个
线程业务过长,业务执行期间锁自动过期被删掉的情况。
分布式锁Redisson-readwritelock
- 读锁与写锁一般搭配使用,写锁期间只能有一个线程在写,其他线程只能等待。写锁完成后,占有读锁的可以一起读。即可以分布式读,不能分布式写。这样的好处就是可以保证读取到的数据一定是最新的。
#写锁
@ResponseBody
@GetMapping("/write")
private String writeHi(){
String s = "";
RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
lock.writeLock().lock();
try {
s = UUID.randomUUID().toString();
redisTemplate.opsForValue().set("write",s);
System.out.println(Thread.currentThread().getId()+"写锁正在写数据");
Thread.sleep(10000);
}catch (Exception e){
e.printStackTrace();
}finally {
lock.writeLock().unlock();
}
return s;
}
#读锁
@ResponseBody
@GetMapping("/read")
private String readHi(){
String s = "";
RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
lock.readLock().lock();
try {
s = redisTemplate.opsForValue().get("write");
System.out.println(Thread.currentThread().getId()+"写锁正在读数据");
}catch (Exception e){
e.printStackTrace();
}finally {
lock.readLock().unlock();
}
return s;
}
- 注意的是,写+读模式,读必须等写完成才能读。读+写,写必须等读完成才能写。写+写阻塞等待。读+读则没关系。
分布式信号量semaphore
- 信号量可以用于限流操作,例如说后期服务最大只能够支持10000个并发,那么可以定义10000个信号量。当获取到信号量的时候即可执行相关代码逻辑,当没有信号量时可以返回错误提示,如:
@ResponseBody
@GetMapping("/park")
private String park(){
//从redis中获取key为semaphore的信号量
RSemaphore semaphore = redisson.getSemaphore("semaphore");
//判断信号量是否还有剩余
boolean b = semaphore.tryAcquire();
if(b){
return "停车成功";
}
return "暂无车位,请稍后再试";
}
@ResponseBody
@GetMapping("/go")
private String go(){
RSemaphore semaphore = redisson.getSemaphore("semaphore");
//释放一个信号量
semaphore.release();
return "欢迎下次光临";
}
分布式锁缓存的一致性问题
- 双写模式
写数据的时候,同时写缓存
可能存在脏数据的问题,最终一致性存在误差
可以通过加锁的方式,保证写数据库与写缓存同一时间只能由一个线程执行,从而实现一致性。
- 失效模式
写数据的时候,同时删除缓存。下一次查询的时候,再更新缓存。
还是存在一致性的问题。还是可以通过加读写锁进行解决。不管是读+写还是写+读的模式,都需要按照顺序执行完毕之后再进行锁内操作。
- 使用canal订阅binlog的方式
SpringCache
springboot整合
- 导入坐标依赖
<dependency>
<groupId>org.springframework.b oot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
- yml配置文件,配置使用redis缓存
spring:
cache:
# 指定缓存类型为redis
type: redis
redis:
# 指定redis中的过期时间为1h,(不应统一设置缓存失效时间,存在缓存雪崩的风险)
time-to-live: 3600000
# 缓存前缀
# 不指定前缀名,就让分区名作为前缀 key-prefix: cache_
# 开启缓存前缀
use-key-prefix: true
# 防止缓存穿透,查询不到数据返回null
cache-null-values: true
- springboot启动类,启用缓存
@EnableCaching
- 配置cache缓存文件
@Configuration
@EnableCaching
public class MyCacheConfig {
@Bean
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
//指定缓存序列化方式为json
config = config.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
//设置配置文件中的各项配置,如过期时间
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixKeysWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}
- 接口上添加@Cacheable()注解
//Cacheable的使用表示当前方法返回的结果需要放入缓存中。如果缓存有该结果,则该接口不调用直接从缓存拿数据,如果没有,则调用该接口并放入缓存
//该注解里面的名字表示将该缓存的结果分到那个区
@Cacheable(value = "{category}", key = "#root.method.name")
@Override
public List<CategoryEntity> getLevel1Categorys() {
List<CategoryEntity> categoryEntities = this.baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
return categoryEntities;
}
SpringCache相关的默认配置:
序列化采用的是jdk自带的序列化,可读性差、兼容性差,应将数据保存为json格式—
过期时间为-1,永不过期—yml配置文件修改存活时间
key名字自动生成—可通过注解的key属性配置自己的key
- @CacheEvict注解。使用该注解可以对缓存进行删除,在缓存一致性的问题下,可以使用该注解达到
失效策略
的使用,即在更新db的时候删除缓存,下次查询的时候再写入缓存。有两个关键的属性值为key和value,要删除哪一块的缓存以及,这两个值就得对应前面新增缓存@CacheAble中key,value的值。
/**
* 缓存失效下CacheEvict的使用
* */
@CacheEvict(value = "category", key = "'getLevel1Categorys'")
@Override
public void updateDetail(CategoryEntity category) {
//先更新自己
this.updateById(category);
//级联更新其他
categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
}
- Caching的使用。使用该注解,可以将多个注解相关的操作合并到一起,一起执行,如下所示:
@Caching(evict = {
@CacheEvict(value = "category", key = "'getLevel1Categorys'"),
@CacheEvict(value = "category", key = "'getCatalogJson2'")
})
- 当然,举个例子,像批量删除,,也可以使用CacheEvict进行批量操作,只需要:
#注意,使用该方式进行批量删除,一定要开启允许使用前缀。use-key-prefix: true
#不然的话进行删除的时候会把所有的key全部删掉
@CacheEvict(value = "category",allEntries = true)
小结
- 读模式
缓存穿透:大量查询一个不存在的数据。解决方案:缓存空数据,可通过spring.cache.redis.cache-null-values=true返回null数据
缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:CacheAble注解中添加sync=true加锁,解决缓存击穿问题。
缓存雪崩:大量的key在同一时间过期。解决:加随机时间。- 写模式
如果对于最终一致性(弱一致性)要求不高,加缓存过期时间即可。如果一致性要求高,可以通过加读写锁的方式解决。