Springboot 拦截器的坑 WebMvcConfigurationSupport 失效

October 16th, 2020 by JasonLe's Tech 54 views

今天遇到一个拦截器失效的问题,具体看源码分析下。

环境:

springboot 2.x

spring 5.x

===================

拦截器创建的几种方式

  • extends WebMvcConfigurationSupport
  • implements WebMvcConfigurer

如果项目中出现了一次 extends WebMvcConfigurationSupport ,其他的 extends WebMvcConfigurationSupport 和 implements WebMvcConfigurer 会失效 。

先看下 WebMvcConfigurationSupport 这个类, addInterceptors 这个方法,默认继承的是 DelegatingWebMvcConfiguration,这个类就是获取 `所有 ` 实现 WebMvcConfigurer 的子类,调用他们的方法,如果有多个 通过实现 WebMvcConfigurer 创建的拦截器,是都可以生效的。

那多个 继承 WebMvcConfigurationSupport 为啥只有一个生效呢,答案在这个类WebMvcAutoConfiguration 的 ConditionalOnMissingBean 注解,只实例化一个Bean,多个继承也只有一个生效。
再看下 addInterceptors 啥时候触发的,获取拦截器的时候,获取过就不再获取了,所以 addInterceptors 在项目启动触发才有效。而 getInterceptors 这个方法是在 handerMapping映射的时候触发的(比如 RequestMappingHandlerMapping、BeanNameUrlHandlerMapping)。

解决方案

针对不同的场景解决方案也不一样,我想到的有3个方案。

  • 不继承 WebMvcConfigurationSupport ,拦截器全部通过实现 WebMvcConfigurer 接口(推荐)
  • 只继承一次 WebMvcConfigurationSupport ,在这个类管理所有的拦截器(不推荐,耦合性太高)
  • 针对我的场景,通过过滤器实现的。注入的代码就不贴了,before 和 fater 方法实现了类似拦截器的 preHandle 和 afterCompletion。有一点需要注意的是指定过滤器的排序(默认已经是最高了,可以忽略),由于过滤器是链式调用,如果想当拦截器用,必须指定最先加载,还有就是过滤器会拦截静态资源,做好对静态资源的放行。

 

https://www.lyscms.info/blog/detail/33A55BEE2FD94E66B40990EA4967D3F7

https://blog.csdn.net/pengdandezhi/article/details/81182701

对缓存更新的思考

August 5th, 2020 by JasonLe's Tech 181 views

缓存更新方案是通过对更新缓存和更新数据库这两个操作的设计,来实现数据的最终一致性,避免出现业务问题。

先来看一下什么时候创建缓存,前端请求的读操作先从缓存中查询数据,如果没有命中数据,则查询数据库,从数据库查询成功后,返回结果,同时更新缓存,方便下次操作。

在数据不发生变更的情况下,这种方式没有问题,如果数据发生了更新操作,就必须要考虑如何操作缓存,保证一致性。

先更新数据库,再更新缓存
先来看第一种方式,在写操作中,先更新数据库,更新成功后,再更新缓存。这种方式最容易想到,但是问题也很明显,数据库更新成功以后,由于缓存和数据库是分布式的,更新缓存可能会失败,就会出现上面例子中的问题,数据库是新的,但缓存中数据是旧的,出现不一致的情况。

先删缓存,再更新数据库
这种方案是在数据更新时,首先删除缓存,再更新数据库,这样可以在一定程度上避免数据不一致的情况。

现在考虑一个并发场景,假如某次的更新操作,更新了商品详情 A 的价格,线程 A 进行更新时失效了缓存数据,线程 B 此时发起一次查询,发现缓存为空,于是查询数据库并更新缓存,然后线程 A 更新数据库为新的价格。

在这种并发操作下,缓存的数据仍然是旧的,出现业务不一致。

先更新数据库,再删缓存
这个是经典的缓存 + 数据库读写的模式,有些资料称它为 Cache Aside 方案。具体操作是这样的:读的时候,先读缓存,缓存没有的话,那么就读数据库,然后取出数据后放入缓存,同时返回响应,更新的时候,先更新数据库,数据库更新成功之后再删除缓存。

为什么说这种方式经典呢?

在 Cache Aside 方案中,调整了数据库更新和缓存失效的顺序,先更新数据库,再失效缓存。

目前大部分业务场景中都应用了读写分离,如果先删除缓存,在读写并发时,可能出现数据不一致。考虑这种情况:

线程 A 删除缓存,然后更新数据库主库;

线程 B 读取缓存,没有读到,查询从库,并且设置缓存为从库数据;

主库和从库同步。

在这种情况下,缓存里的数据就是旧的,所以建议先更新数据库,再失效缓存。当然,在 Cache Aside 方案中,也存在删除缓存失败的可能,因为缓存删除操作比较轻量级,可以通过多次重试等来解决,你也可以考虑下有没有其他的方案来保证。

对缓存更新的思考

为什么删除而不是更新缓存
现在思考一个问题,为什么是删除缓存,而不是更新缓存呢?删除一个数据,相比更新一个数据更加轻量级,出问题的概率更小。

在实际业务中,缓存的数据可能不是直接来自数据库表,也许来自多张底层数据表的聚合。比如上面提到的商品详情信息,在底层可能会关联商品表、价格表、库存表等,如果更新了一个价格字段,那么就要更新整个数据库,还要关联的去查询和汇总各个周边业务系统的数据,这个操作会非常耗时。

从另外一个角度,不是所有的缓存数据都是频繁访问的,更新后的缓存可能会长时间不被访问,所以说,从计算资源和整体性能的考虑,更新的时候删除缓存,等到下次查询命中再填充缓存,是一个更好的方案。

系统设计中有一个思想叫 Lazy Loading,适用于那些加载代价大的操作,删除缓存而不是更新缓存,就是懒加载思想的一个应用。

多级缓存如何更新
再看一个实际应用中的问题,多级缓存如何更新?

多级缓存是系统中一个常用的设计,服务端缓存分为应用内缓存和外部缓存,比如在电商的商品信息展示中,可能会有多级缓存协同。那么多级缓存之间如何同步数据呢?

常见的方案是通过消息队列通知的方式,也就是在数据库更新后,通过事务性消息队列加监听的方式,失效对应的缓存。多级缓存比较难保证数据一致性,通常用在对数据一致性不敏感的业务中,比如新闻资讯类、电商的用户评论模块等。上面的内容是几种常用的缓存和数据库的双写一致性方案,大家在开发中肯定应用过设计模式,这些缓存应用套路和设计模式一样,是前人在大量工程开发中的总结,是一个通用的解决范式。在具体业务中,还是需要有针对性地进行设计,比如通过给数据添加版本号,或者通过时间戳 + 业务主键的方式,控制缓存的数据版本实现最终一致性。

 

Elasticsearch 性能优化-移除FST堆内存

July 29th, 2020 by JasonLe's Tech 236 views

单节点es能存储和处理的数据量主要受3方面限制:cpu、内存、磁盘

es是java程序,内存受限于JVM的堆内存,给es进程的堆内存又不能超过32G(受限于指针压缩,超过32g指针压缩失败,内存浪费)。所有堆内存大小的限制是es处理能力的主要限制

分析es堆内存,发现es堆内存使用占比最多的是FST。

何为FST呢?

倒排索引以分词后的词为主键进行组织,每个词后面对应的都是存在该关键字的文档id(这些id使用增量编码压缩,将大数变小数,按字节存储。所以如果id有一定的公共前缀,可以增加压缩比),term查询首先就是找到词,然后再根据文档id找到文档记录。如果我们每次查找都去找倒排索引,由于倒排索引存储在磁盘上,那就需要遍历磁盘上倒排索引记录,会进行多次磁盘IO,严重影响查询性能。

联想:如果索引中查询频繁的字段值有公共前缀,那在分词表是不是就比较接近,比较容易分配到同一个block中,可以加快查询速度

而FST相当于是倒排索引的一个二级缓存索引树,在生成倒排索引之后,根据分词表,将原先的分词表划分为多个block,每个block包含25-48个词,将每个block里的词的公共前缀取出来作为作为1个节点,其叶子节点对应是block的首地址,形成一个前缀树,放在堆内存中,永不回收,这就是FST。(可以联想到如果节点上的段很多,这部分占用的堆内存将会很多)

在term查找的时候,先根据term关键字遍历堆内存中的FST,找到对应term关键字匹配的前缀节点,找到该前缀节点下的block首地址。再读取本地磁盘上的分词表,将对应的block读取到内存,找到term关键词对应的文档id,然后根据文档id找到磁盘上的原始文档数据

在refresh和merge生成新的段时,会在磁盘生成一系列的段文件,其中有一个.tip文件就是FST文件,生成tip文件之后,lucene会将每个字段的FST(es里会对索引结构里的每个设置为index的字段创建倒排索引和FST)数据解析处理,永驻在堆内存里,直到段被删除。

优化方案:
1、扩大FST中每个block包含的词的数量,block变少,FST中占有的内存也会下降–会降低查询性能

2、将FST从堆内存移到堆外内存,交给MMAP管理,nmap属于page cache,会被回收,在大量读写下,会频繁被回收,频繁从磁盘读tip文件,性能反而大大下降

3、将FST从堆内存移到堆内存,新建一个cache来管理,使用LRU策略,进行缓存更新。
每次根据key去cache中找到FST,返回FST对象,拷贝加载到对内内存,进行查找。

4、将FST从堆内存移到堆内存,新建一个cache来管理,使用LRU策略,进行更新
每次根据key去cache中找到FST,返回FST对象地址,在cache中直接进行查找。

Elasticsearch 对于分片的处理

July 24th, 2020 by JasonLe's Tech 156 views

elk在我们的生产环境中有广泛的应用,最近发现elk查询速度缓慢,最后查阅了大量资料,发现分片数量对查询性能有巨大影响,但是由于是线上系统,所以对于数据的处理比较谨慎。

1. 创建共享目录
在三个节点上创建共享目录/mnt/backup/,使三个节点的es用户都可以读写该目录。(推荐使用nfs创建共享目录)

2. 修改ES配置
在elasticsearch.yml中加入一行: path.repo: [“/home/backup/”],修改后重启es集群。

3. 创建备份仓库

PUT /_snapshot/fs_backup
{
  "type": "fs",
  "settings": {
    "location": "/home/app/elk_data",
    "compress": true
  }
}

PUT /_snapshot/fs_backup/snapshot_indicator_value?wait_for_completion=true
{
  "indices": "xxxxxx",
  "ignore_unavailable": true,
  "include_global_state": true
}

由于这个是一个耗时的操作,所以我们可以通过 GET _cat/tasks?detailed 查看备份状态。备份完毕以后,我们使用reindex进行分片,yyyy必须是事先创建好

{
...
  "settings": {
    "number_of_shards": 5,
    "number_of_replicas": 1
  }
...
}

 

POST _reindex?slices=auto&refresh
{
  "source": {
    "index": "xxxx"
  },
  "dest": {
    "index" : "yyyy"
  }
}

这个reindex也是一个耗时操作,需要等待执行完毕,执行完毕以后,使用alias别名,访问xxxx即可,通过迁移,发现api耗时降低一半,用户体验明显变好了。

DELETE xxxx
GET /_aliases
POST /_aliases
{
  "actions": [
    {
      "add": {
        "index": "yyyy",
        "alias": "xxxx"
      }
    }
  ]
}

参考:

https://www.elastic.co/guide/en/elasticsearch/reference/current/snapshots-register-repository.html

 

设计微信朋友圈的社交系统

July 19th, 2020 by JasonLe's Tech 275 views

你自己可以发朋友圈,刷朋友,看到你自己发的朋友圈以及你的好友发的朋友圈,你可以对朋友圈进行点赞,进行评论,你可以去设置权限,你不看某些人的朋友圈,不让某些人看你的朋友圈,拉黑或者删除某个好友再也不用看到他的朋友圈了,首先你发送朋友圈的时候,一般是9张图片配合一些文字,组成了一条朋友前,文字还好说,但是图片就会稍微有点大了,也可以是一个短视频配合一些文字,点击发送,假设你要同步发送,可能会导致你点击发送按钮之后,弹出一个旋转框,告诉你发送中,持续好几秒种中,用户体验是比较差的。

有一个好一点的办法,可以把这些数据在客户端本地暂存一下,然后直接让你发送成功返回,走一个异步发送状态,然后立马让你自己在刷朋友圈的时候,可以把你客户端本地的刚发的朋友圈加载出来看到仅仅是这样,就会变成,发朋友圈成了你自己的自娱自乐,因为你的朋友圈并没有发送出去让你的好朋友看到,可以走一个异步的模式,把你的朋友圈里的图片或者视频+文字,花费几秒钟的时间传送到你的后台服务器上去存储之后,你的朋友就可以从后台服务器上加载你的朋友圈里的图片和视频,可以看到了你的那些视频和图片,是不是可以就都直接就近上传到CDN(content delivery network),不是直接到朋友圈系统的后台,这样速度是很快的;

接着就是发送请求到朋友圈后端系统,请求包括图片的地址,你配的文字,发朋友圈的时候可以选择开放给谁看,这些数据写入到朋友圈发布表里去然后需要在相册表里写入索引数据,里面存放的是对你的发布表里的数据引用,这样你以后浏览相册的时候,都是根据相册里的索引数据到发布表里找实际对应的数据的接着就走一个离线批处理,通过批处理程序把这条朋友圈写入到你所有好友的时间线表里去,你好友的时间线表里就是存放了他刷朋友圈的时候,可以按照时间线刷到的所有好友的朋友圈你有3个好朋友,每个好朋友都在时间线表里有一个朋友圈的时间线,按照时间顺序排列了他可以查看的所有朋友圈,包括了他自己发的朋友圈以及他的好友发的朋友圈允许他看的那些,都会在这里然后你的好朋友刷朋友圈的时候,就会知道自己的时间线表里有个新的变化,就是有好友发了朋友圈,此时就会提示你一个红色的圆点,你就开始刷,刷的时候就根据图片url地址去cdn拉取。

 

权限控制

这条朋友圈的权限到了后台之后,会有一个离线批处理的程序跑起来,对最近发的一波朋友圈都找他们的朋友圈的权限的设置看一下,此时就会对你允许看到的好友,此时就在他们的时间线里插入这条朋友圈数据,那么这样的话,只有你允许的好友的时间线里才有你这条朋友圈

 

比如说王五发的朋友圈16931可以允许张三和李四看到,设置了一个标签组,标签名称是老铁三人组,里面就正好有张三和李四

 

张三       发表朋友圈的时间戳           朋友圈16931        王五

张三       发表朋友圈的时间戳           朋友圈16384        李四

李四       发表朋友圈的时间戳           朋友圈16931        王五

 

在redis里可以设置张三的朋友圈是有变动的一个状态,在上次拉取朋友圈的时间点之后的一些朋友圈都从时间线表里拉取出来,刷朋友圈的时候,如果说你的网速要是不太好的话,你会发现这样一个场景

就是你最新的一些朋友发的朋友圈是显示出来了,但是视频和图片都是一片灰色,仅仅能看到他的文字和其他的一些东西,比如说点赞之类的,图片和视频死活看不到,都是一片灰色,反正我自己网速不好的时候经常看到这样的情况

假设王五之前发了一条朋友圈,设置李四可以看到的,李四之前确实是看到了这条朋友圈的,但是有个问题,王五后来跟李四吵了一架,关系变得非常的不好,王五就对李四设置了一个朋友圈的权限,就是自己的朋友圈不允许李四看到,甚至可能会直接拉黑/删除李四这个好友,这个就够狠了

你设置自己的朋友圈对所有朋友都是仅仅三天之内可见

就是说你跟李四之间的朋友圈的权限总设置或者是朋友之间的关系,有了变化,或者是你的自己的朋友圈对外展示的总权限有了变化,此时每次如果有变动,那么这些设置,包括你对每个朋友的朋友圈权限的设置,跟朋友的关系,自己的朋友圈的总权限,这些设置都会统统的缓存起来包括缓存在你自己的客户端本地,也可以缓存在你的朋友的客户端本地。

但是你可能随时会拉黑、删除某个人,或者是突然设置对那个人朋友圈不可见,或者是突然你自己设置了朋友圈三天可见什么的,所以你设置的这些东西,都会被缓存起来,每次你好友刷朋友圈,查看自己的时间线表的时候,都会检查你的某条朋友圈根据你的一些行为,是否还对他可见。

李四会关注王五的各种朋友圈权限和朋友关系的一个变化,一旦说有变化了,可以缓存到自己的本地,下次在客户端里再次刷新朋友圈的时候,客户端对于王五的朋友圈会和王五的各种权限设置结合起来判断一下李四能否看到王五的这条朋友圈

点赞系统

我看到了你的朋友圈,此时我就可以对你的朋友圈去进行一个点赞,也可以取消点赞,假设要设计成支撑高并发的点赞系统,应该如何设计?

朋友圈的点赞和评论,是独立的数据,其实比如点赞,都是可以基于redis来做的,每个朋友圈里对应一个set数据结构,里面放谁给你点赞了,这样每条朋友圈的点赞人(smembers)和点赞数量(scard)直接从redis出就可以了。

评论也是可以存表里的,都是以朋友圈为粒度来存储。

那么刷朋友圈的时候,比如说你好友和你,另外一个好友都是好友,此时你好友刷到了你的朋友圈,就可以把另外一个好友对你的点赞和评论都拉出来,展示在客户端下面就可以了,这个展示过程可以是动态的。

你是王五,你的朋友圈被张三点赞了,李四跟你们也是好朋友,此时李四刷朋友圈看到了王五发的这条朋友圈,此时你可以在后台,对这条朋友圈的set用张三做一个sismember操作,就是判断一下你们俩的所有共同好友,有哪些人对这条朋友圈点赞了。

此时就可以看出来这条朋友圈被你们的共同好友多少人点赞了,哪些人点赞了。

比如你另外一个好友是否对这条朋友圈点赞了,直接sismember就可以判断出来,这样整个你基于redis,他都是非常高性能的。