博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
微服务进阶学习整合高级篇--缓存与分布式锁
阅读量:2047 次
发布时间:2019-04-28

本文共 13985 字,大约阅读时间需要 46 分钟。

微服务进阶学习整合高级篇--缓存

缓存的使用选择条件

  • 即时性、数据一致性要求不高的
  • 读的频率高,修改的频率少的

redis基本使用

缓存中间件redis入门使用

  • 引入redis坐标
#这里注意springboot版本,springboot稍微旧一点的版本不需要加上-data-字样
org.springframework.boot
spring-boot-starter-data-redis
  • yml配置redis相关信息
  • 注入需要使用的redistemplate,也可以根据自己的需求配置对应的redisTemplate。可以在自定义redisTemplate中进行序列化与反序列化,也可以在代码中每次书写的时候手动序列化
@Override    public Map
> getCatalogJson2(){
//先去redis中查询缓存是否存在需要读的数据 String catalogJson = redisTemplate.opsForValue().get("catalogJson"); //如果读不到缓存,缓存不存在 if(StringUtils.isEmpty(catalogJson)){
//去db读取缓存 Map
> catalogJson2FromDB = this.getCatalogJson2FromDB(); //将查询的数据序列化成json对象,设置缓存方便下次使用 String cacheCatalog = JSON.toJSONString(catalogJson2FromDB); redisTemplate.opsForValue().set("catalogJson",cacheCatalog); return catalogJson2FromDB; } //如果查到缓存,则读取缓存,将缓存的json转换成对象,使用匿名内部类将json转换成指定的类型。 Map
> result = JSON.parseObject(catalogJson, new TypeReference
>>(){
}); 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
> getCatalogJson2(){
//先去redis中查询缓存是否存在需要读的数据 String catalogJson = redisTemplate.opsForValue().get("catalogJson"); //如果读不到缓存,缓存不存在 if(StringUtils.isEmpty(catalogJson)){
System.out.println("缓存不命中,查询数据库"); //去db读取缓存 Map
> catalogJson2FromDB = this.getCatalogJson2FromDB(); return catalogJson2FromDB; } System.out.println("缓存命中,没有查询数据库"); //如果查到缓存,则读取缓存,将缓存的json转换成对象,使用匿名内部类。在这里也可以通过自己配置的redisTemplate,在配置文件中统一对数据进行序列化与反序列化处理 Map
> result = JSON.parseObject(catalogJson, new TypeReference
>>(){
}); return result; }public Map
> getCatalogJson2FromDB() { /** * 本地锁的方式锁住进程,只允许一个进程进来查看数据,防止高并发下缓存击穿的问题。默认单例模式因此只会存在一个getCatalogJson2FromDB资源 * 双重校验锁,确保进来的线程不会重复查询数据库 * 分布式情况下本地锁方式还是会存在问题,假如说日后服务是集群的形式,有8台product服务,每一台服务都会只会锁住当前服务的当前线程,还是会存在多个服务访问统一资源 * */ synchronized (this){ String catalogJson = redisTemplate.opsForValue().get("catalogJson"); //双重校验锁,如果不为空则去查缓存,为空则继续执行查询数据库的逻辑 if(!StringUtils.isEmpty(catalogJson)){ Map
> result = JSON.parseObject(catalogJson, new TypeReference
>>(){ }); 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坐标

org.redisson
redisson
3.13.4

2、编写配置文件

@Configurationpublic 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整合

  • 导入坐标依赖
org.springframework.b oot
spring-boot-starter-cache
  • 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@EnableCachingpublic 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
getLevel1Categorys() {
List
categoryEntities = this.baseMapper.selectList(new QueryWrapper
().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在同一时间过期。解决:加随机时间。
  • 写模式
    如果对于最终一致性(弱一致性)要求不高,加缓存过期时间即可。如果一致性要求高,可以通过加读写锁的方式解决。

转载地址:http://ccqof.baihongyu.com/

你可能感兴趣的文章
阿里云《云原生》公开课笔记 第六章 应用编排与管理:Deployment
查看>>
阿里云《云原生》公开课笔记 第七章 应用编排与管理:Job和DaemonSet
查看>>
阿里云《云原生》公开课笔记 第八章 应用配置管理
查看>>
阿里云《云原生》公开课笔记 第九章 应用存储和持久化数据卷:核心知识
查看>>
linux系统 阿里云源
查看>>
国内外helm源记录
查看>>
牛客网题目1:最大数
查看>>
散落人间知识点记录one
查看>>
Leetcode C++ 随手刷 547.朋友圈
查看>>
手抄笔记:深入理解linux内核-1
查看>>
内存堆与栈
查看>>
Leetcode C++《每日一题》20200621 124.二叉树的最大路径和
查看>>
Leetcode C++《每日一题》20200622 面试题 16.18. 模式匹配
查看>>
Leetcode C++《每日一题》20200625 139. 单词拆分
查看>>
Leetcode C++《每日一题》20200626 338. 比特位计数
查看>>
Leetcode C++ 《拓扑排序-1》20200626 207.课程表
查看>>
Go语言学习Part1:包、变量和函数
查看>>
Go语言学习Part2:流程控制语句:for、if、else、switch 和 defer
查看>>
Go语言学习Part3:struct、slice和映射
查看>>
Go语言学习Part4-1:方法和接口
查看>>