超额收益

November 1st, 2020 by JasonLe's Tech 1,460 views
        首先,收益一般分为两部分,第一部分是跟着市场上涨和下跌的,这部分收益我们称为贝塔收益,比如持有沪深300ETF,然后沪深300指数涨了10%,你就赚了10%,这部分收益其实和能力无关,算是持有资产给的系统性收益吧。
        另外一部分是不随着系统变化的收益,我们称之为阿尔法收益(α),比如沪深300指数不涨不跌,但是你持有了一个什么沪深300增强基金,涨了+3%,这部分收益就是不随着系统收益来的收益,就是我们常说的阿尔法收益,也就是所谓的超额收益。
在对到基金上来,指数基金大多是跟着一个具体的市场指数,这个指数涨这个基金就涨,这种被动指数基金一般都是赚取的是贝塔收益(β)。而与之对应的主动基金,自然就是靠基金经理的能力来追求超额收益了。
        这里要说两件事情,第一是贝塔收益和阿尔法收益并不冲突,比如优秀的主动基金在市场行情上涨的时候大概率也会上涨,并且会同时有超额收益,这部分相当于赚了阿尔法+贝塔两种收益。
        第二个事情就是,这两个收益也没有优劣之分。这句话有两层意思,第一层意思是,不管是贝塔还是阿尔法,赚取的收益都对应着相应的风险,这一点是一样的。第二层意思是,不是说追求超额收益就会一定比市场的贝塔收益赚的多,主动基金可能跑赢指数,但是也可能跑输指数,尤其是牛市的时候。因此其实并没有优劣之分。
        那我们投资者应该怎么追求收益呢。其实看起来,大部人追求的都是在享受到贝塔收益的基础上也能有超额收益。因为贝塔收益其实并不由我们决定,市场涨不涨涨多少什么时候涨我们并不知道,但是只要我们在场,贝塔收益(当然也可能是亏损)就一定能享受到。那么超额收益,我们应该如何追求呢?
       当然这里先抛两个基本的原则,
  • 是收益风险同源,就是想要赚更多的超额收益肯定会承担更多的风险
  • 偏离度越高,超额收益的可能就越高。这个也好理解,买一个股票可能一个月翻倍,买下沪深300的全部股票,大概率也就是复制沪深300的走势了。
但是请记住第一条,偏离度越高,是超额收益的可能越高,而不是超额收益越好,偏离度越高承担的风险自然也是越大的。好啦,下面开始分享超额收益从哪里来。一般超额收益是从下面几个方面来的,当然也是一个递进的层次关系。
  • 第一层,行业内优选个股。比如同一个行业内,你能选出来A公司要比其他公司好,这个时候买入A公司,大概率就会获得超过该行业整体收益的超额收益。对应到基金里面,我们经常听到的基金公司的行业研究员就是干这个工作,研究某个行业内的不同公司的好坏来完成选股,从而来获得行业内选股的超额收益。
  • 第二层,行业轮动。你要问上面的行业研究员这个行业A好还是B好,他会很明确的告诉你,但是你要问他,这个行业啥时候涨?他就不一定知道了。行业之间的走势和轮动就要比上面优选个股要高一个层次,这个需要考虑比如行业的整个供应链、上下游传导,比如汽车行业就要考虑比如人口、历史汽车销量和折旧周期、油价、甚至高速公路、高铁等的开设维修,环保政策等等,如果预测出后市汽车行业销量要上涨,那肯定要先买比如汽车零部件供应商的股票,然后才是汽车厂商本身,然后才会轮到到比如油企、高速等。这个行业轮动是一般是由基金经理来做。这部分做的好了超额收益会高,相当于每次都享受了当前涨的最好的行业。
  • 第三个层面,仓位控制。比如市场高点就减仓,低点加仓,这是最简单的仓位控制。牛逼的人一般都是做大类资产配置,比如资金在A股、美股、港股、债券、黄金、甚至不用币种之间转换,这就是所谓的永不空仓。这个需要对整体宏观经济和不同资产都有掌握,如果只是仓位高点的控制,一般也是由基金经理来做。但是涉及到全球资产配置,这就不是单个基金经理能搞的了,一般都是金融大鳄弄的,比如之前做空泰铢引发亚洲金融危机的索罗斯就是这么干的。
         行业内选股,这就是我们常说的选股,仓位控制就是我们常说的择时,而第二个行业轮动,既包含了选股也包含了择时。听起来好像我们平时都是这么干的,但是据分析,就算是专业的基金经理,拉长周期来看,也就是在第一个层面优选个股上做确实不错,至于行业轮动和仓位控制,就算是专业的基金经理平均下来也是搞的细碎。所以前几年指数基金几乎完全碾压了主动基金。当然这几年主动基金有点抬头,看看后面会怎么样吧。
https://zhuanlan.zhihu.com/p/171641265

Springboot 拦截器的坑 WebMvcConfigurationSupport 失效

October 16th, 2020 by JasonLe's Tech 14,930 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 1,561 views

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

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

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

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

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

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

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

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

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

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

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

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

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

主库和从库同步。

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

对缓存更新的思考

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

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

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

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

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

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

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

 

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

July 29th, 2020 by JasonLe's Tech 2,209 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 1,642 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