MemSQL简介

发布在 MemSQL

为什么要看MemSQL

因为 MemSQL 自称突出一个快!而我们团队的数据平台正需要一个ms级别查询的数据库,因此花了一点时间来调研 MemSQL 。

初体验结果

MemSQL与我们期望的ms级别查询相差甚远,最简单的查询也通常是500ms返回,并不能应对线上的高并发实时查询系统。秒级查询已经有presto等各种大数据组件,对于我们团队已经没有理由再来深入使用MemSQL。

MemSQL 其实有很多优点,有完善的文档,精致的页面管理工具,方便的安装,甚至一键导入大量数据来方便用户体验 (就差把sql的耗时贴出来了,要是贴出来就省了我调研的时间了),可惜在同一梯队还有太多免费、开源、有活跃社区的竞争对手。

特点

  • MemSQL属于商业公司,免费4 units. (每8核32G 为1 unit),不开源
  • 自带portal界面管理,MemSQL Studio,自带安装工具
  • 完全支持mysql语法,jdbc,增删改查
  • 支持 in-memory rowstore && on-disk columnstore
  • 大吞吐量数据写入(loading TPC-H SF100 (approximately 100 GBs of row files) will take around four minutes)
  • exactly-once
  • aggregator节点运行sql,聚合结果;leaf节点存储&处理数据

数据导入

  1. file , 指定分隔符
  2. Streaming (Kafka , S3 , Azure Blob , Filesystem) 通过Pipelines
  3. MySQL (sql文件)

选择 shard key (类似ES的routing)

  • Using a column or set of columns unique enough to minimize skew.

    独特的key,使数据均匀分布

  • Sharding on columns which you expect to filter or join on often. This allows the optimizer to minimize network traffic during the execution of the query (see [Distributed DML] for more details).

    使用你经常要join或过滤的字段,减小网络传输。比如订单相关,使用user_id

查询调优

  1. 加index 优化过滤 groupby sort
  2. shard key : Gather partitions:all 优化为 Gather partitions:single
  3. Reference Tables 小表,不常更新 ,每个节点都有复制

profile

表结构优化

Rowstore vs. Columnstore

https://docs.memsql.com/tutorials/v6.8/optimizing-table-data-structures/

Rowstore : 查询指定列,并发更新。使用无锁索引,支持多索引。频繁更新的事务场景。unique constraints 。同样会往disk写一份 用于恢复。

Columnstore: 顺序扫描,单索引。适用场景:聚合仅很少列(10列内),扫描大量行,很少频繁单行删改,更新应该是大批量的修改

分布式SQL

DDL

  • 每一个并行的查询都在每个leaf的每个partition上分开执行。
  • 默认每个leaf的partition数量=cpu数量
  • 每个partition都是个单独的库
  • reference tables在每个aggregator和leaf都有一份复制,适合小表
  • shard key 决定数据落到哪一partition
  • 主键和unique key 需要包含shard key ,例如:
1
2
3
4
5
6
7
CREATE TABLE clicks (
click_id BIGINT AUTO_INCREMENT,
user_id INT,
page_id INT,
SHARD KEY (user_id),
PRIMARY KEY (click_id, user_id)
);

DML

  • 分布式join 相同的shard key来join可以提升性能
  • 分布式事务,等待每个partition都ready,再一起提交
  • 引用表必须具有显式主键。 分布式表中的AUTO_INCREMENT列必须是BIGINT。 自动增量值在每个聚合器上单调递增,但在整个群集中不连续。 分片表不支持唯一键(除非唯一键是分片键的超集)。 分片表不支持修改分片键的UPDATE查询。 分片表不支持UPDATE … LIMIT。

遇到问题

  1. mysql front连接不上

    换用其它客户端可以,比如idea

  2. https://docs.memsql.com/tutorials/v6.8/build-stock-trade-database/ 导入csv文件提示找不到文件

    docker cp /home/hxy/download/companylist.csv 62b8df2d8761://usr/share

性能体验

测试机器

AWS 8c32g

测试数据

官方S3的数据,我们使用 columnstore 和 rowstore 各测一次sql查询。

https://docs.memsql.com/guides/latest/load-data/pipelines/step-1/

columnstore数据

columnstore.png

rowstore 数据

rowstore.png

由于150W行数据存disk 占1G,存memory需要占5G,发现机器内存不足,只导入了一张150W行和一张25行的表。

执行sql耗时

1
select * from nation limit 20;

columnstore 770ms

rowstore 700ms

1
2
3
4
5
6
7
8
9
10
11
12
13
14
select l_returnflag,
l_linestatus,
sum(l_quantity) as sum_qty,
sum(l_extendedprice) as sum_base_price,
sum(l_extendedprice * (1 - l_discount)) as sum_disc_price,
sum(l_extendedprice * (1 - l_discount) * (1 + l_tax)) as sum_charge,
avg(l_quantity) as avg_qty,
avg(l_extendedprice) as avg_price,
avg(l_discount) as avg_disc,
count(*) as count_order
from lineitem
where l_shipdate <= date('1998-12-01' - interval '90' day)
group by l_returnflag, l_linestatus
order by l_returnflag, l_linestatus;

columnstore 19s 5s 5s

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
select
l_orderkey,
sum(l_extendedprice * (1 - l_discount)) as revenue,
o_orderdate,
o_shippriority
from
customer,
orders,
lineitem
where
c_mktsegment = 'BUILDING'
and c_custkey = o_custkey
and l_orderkey = o_orderkey
and o_orderdate < date('1995-03-15')
and l_shipdate > date('1995-03-15')
group by
l_orderkey,
o_orderdate,
o_shippriority
order by
revenue desc,
o_orderdate
limit 10;

columnstore 10s 5s 5s

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
select
c_custkey,
c_name,
sum(l_extendedprice * (1 - l_discount)) as revenue,
c_acctbal,
n_name,
c_address,
c_phone,
c_comment
from
customer,
orders,
lineitem,
nation
where
c_custkey = o_custkey
and l_orderkey = o_orderkey
and o_orderdate >= date('1993-10-01')
and o_orderdate < date('1993-10-01') + interval '3' month
and l_returnflag = 'R'
and c_nationkey = n_nationkey
group by
c_custkey,
c_name,
c_acctbal,
c_phone,
n_name,
c_address,
c_comment
order by
revenue desc
limit 20;

columnstore 12s 9s 9s

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
select
nation,
o_year,
sum(amount) as sum_profit
from
(
select
n_name as nation,
extract(year from o_orderdate) as o_year,
l_extendedprice * (1 - l_discount) - ps_supplycost * l_quantity as amount
from
part,
supplier,
lineitem,
partsupp,
orders,
nation
where
s_suppkey = l_suppkey
and ps_suppkey = l_suppkey
and ps_partkey = l_partkey
and p_partkey = l_partkey
and o_orderkey = l_orderkey
and s_nationkey = n_nationkey
and p_name like '%green%'
) as profit
group by
nation,
o_year
order by
nation,
o_year desc;

columnstore 32s 19s 19s

1
2
3
4
5
6
select *
from customer c
left join orders o on c.c_custkey = o.o_custkey
left join nation n on c.c_nationkey = n.n_nationkey
where c.c_custkey = '8367'
limit 10;

columnstore 32s 4s 4s

1
2
3
4
5
6
select *
from customer c
left join orders o on c.c_custkey = o.o_custkey and o_custkey = '2141'
left join nation n on c.c_nationkey = n.n_nationkey
where c.c_custkey = '2141'
limit 10;

columnstore 2s 1s 1s

1
2
3
4
select *
from customer c
where c.c_custkey = '2141'
limit 10;

rowstore 500ms

评论和共享

说起 ElasticSearch,往往大家想到的都是 ELK 的一套,但是作为 NoSQL,ES 有极快的响应速度,强大的聚合功能,支持复杂的查询条件,应对高并发的复杂查询的业务场景其实也是非常强力的。

You Know, for Search

我们团队就一直使用 ES 作为主力数据库, 从一开始做全文检索,到现在承担全部的商品列表页查询。近几个月将查询系统的 qps 从 1k 优化到了 10k+,其中 ES 的优化占了很重要一部分,准确的来说,应该是对 ES 特性的扬长避短起到了非常大的作用。

数组 & 嵌套结构

ES 没有 join,很多人直接就会认为 ES 无法处理一对多的情况,其实还有数组嵌套结构可以应付常见的业务场景。

比如一个商品拥有多种属性,都存放在一个数组字段中,使用 must 和 must_not 就可以灵活地进行查询筛选。

比如同款不同色的几件T恤,使用嵌套结构保存,搜索时只需要其中一件满足筛选条件,便可以全部带出来,在页面上以多个小色块展示,而无需占用多个展示位。并且还可以拿满足筛选条件的商品中的某属性最大值/最小值等进行排序,如官网给出的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
{
"query": {
"nested": {
"path": "parent",
"query": {
"bool": {
"must": {"range": {"parent.age": {"gte": 21}}},
"filter": {
"nested": {
"path": "parent.child",
"query": {"match": {"parent.child.name": "matt"}}
}
}
}
}
}
},
"sort" : [
{
"parent.child.age" : {
"mode" : "min",
"order" : "asc",
"nested": {
"path": "parent",
"filter": {
"range": {"parent.age": {"gte": 21}}
},
"nested": {
"path": "parent.child",
"filter": {
"match": {"parent.child.name": "matt"}
}
}
}
}
}
]
}

聚合

商品列表页面能用到聚合的场景非常多,比如聚合出分类下(可能多达数万个商品)的各子分类,各属性的数量,并且需要支持复杂的筛选条件,比如库存,价格范围等等,并且这种查询速度远比 RDS 的 join + group by + count 快。

又比如需要查出最近10天内有新商品的日期列表,那就可以用到 date_histogram 聚合函数。

动态字段

动态字段的设计也为我们的业务提供了很大便利,由于与具体业务关联性太强,就不详细展开了。

ES能支持的动态字段数量非常的多,不过这里要留意的就是动态字段一个比较容易出问题的地方,就是瞬时写入大量的动态字段会导致集群索引的元数据大量变动,master 节点负载暴涨甚至挂掉。

缺陷

  1. 没有 join。ES 的查询速度非常的快,但是不能 join 毕竟还是有一些业务场景无法使用。当然话又说回来,在高并发量下,多表 join 能不能抗得住也是个问题。对于查询,我们一贯的原则还是:把数据离线准备成便于查询的结构,线上实时查询尽可能的简单,一步到位
  2. 由于要把数据离线准备好,这便带来了数据同步更新的问题,数据的时效性、准确性都需要保证,数组与嵌套结构的数据更新也不够方便高效,这些都会增加很多的工作量。

评论和共享

集群管理

节点分配

原集群 master*3 data*12 client*0

新集群 master*3 data*12 injest*0 coordinating*0

ingest 节点用于支持 pipeline 操作 对bulk和index文档进行预处理

coordinating 功能主要是分发请求,聚合各节点的处理结果,负载均衡,大规模集群可以设置一个给读,一个给写。但coordinating 数量也不宜过多,会拖慢选举主节点的时间,并且data节点其实也可以处理这些请求.

节点设置

search.remote.connect: false
node.ingest: false

数据迁移

数据源

由于有数据源及同步方案,所以只需数据全量导入6.3版本的集群即可.

索引管理

目前生产环境有300个索引需要同步,要检查同步脚本的创建索引,切别名等步骤.

mapping设置

  1. type 只支持1种,自 ES7.0 起将不再支持 type—官方说明
  2. 对可以使用自增 id 的索引使用自增 id
  3. 对大多数字符串字段使用 keyword 类型
  4. 对不用于数值范围查找的数值类型改为keyword类型
  5. 分词插件可能需要改动
  6. index: no 改为 index:false
  7. index: not_analyzed 删掉

提高迁移速度

  1. sudo swapoff -a
  2. 副本设置为0
  3. refresh_interval 设置为 -1 (对线上生产集群上索引批量导入时,设置-1后,重新打开时可能会导致集群压力暴增)
  4. 导入数据
  5. refresh_interval 设置为30
  6. 确认数据正确性
  7. POST /_forcemerge max_num_segments=1(对于大索引可能非常耗时)
  8. 副本设置为1

scala项目升级

  1. scala & play 升级, 尤其是play的升级会导致大量代码改动
  2. elastic4s 依赖升级,注意除了core包还需要http包 。
  3. 原本的获取client, 构建dsl,excute,解析response的大量代码要修改,尤其是构建dsl涉及大量业务,需要逐一比对修改。

监控

Prometheus + Grafana 主要是获取ES信息的api随之升级,改动通常不大

另外推荐 xpack 的 monitor,收集了 segment 的数据,收集了每个索引的请求量,响应时间等信息,信息集成进了 kibana

评论和共享

问题

Service层注入Dao时, Intellij 总会以红色波浪线提示我们

1
2
@Autowired
private UserDao userDao;

Could not autowire. No beans of ‘UserDao’ type found.
Checks autowiring problems in a bean class.

尽管我们都知道 Dao 层的 Bean 实际上都是有的,并且可以设置关闭这恼人的提示,但是我们有没有想过为什么 Intellij 就找不到这个 Bean 呢?甚至有人有这种做法

1
2
3
@Repository
public interface UserDao {
}

来避免提示,但是这种做法正确么?

所以今天我们的疑问就是

  1. 为什么 Dao 层不需要加 @Repository 注解,源码里到底做了什么?
  2. 加了 @Repository 注解有什么影响?

答案

  1. 关键在于 ClassPathMapperScanner 对指定包的扫描,并且扫描过程对 Spring 原本的扫描 Bean 的步骤 “加了料” ,Spring 本身只扫实现类,但 MyBatis 的扫描器扫了接口 。并且扫完接口之后,为接口配了个 BeanDefinition ,并且这个 bd 的 BeanClass 是 MapperFactoryBean

    对于 BeanDefinition 和 MapperFactoryBean 不了解的同学请查询相关资料和源码

  2. 仅仅只能解决 Intellij 静态查找 bean 的问题,没有实际作用。即使加了注解,比如@Controller,@Service 等等,也会被 Spring 的扫描器给忽略掉,因为扫描器会过滤掉接口

源码探索

下面的源码部分如果读者提前有 MyBatis 的 Bean 的执行流程,和 Spring 的 Bean加载的相关知识就更好理解。

1. 分析问题

关于为什么不需要注解就能获取到 Dao 层的 Bean,看似答案很简单,因为配置了扫描指定这个包里的 xxxDao.class 啊,比如使用注解 @MapperScan(“com.example.dao”)。

这个答案太过表面,觉得问题简单只是因为对 Spring 的 Bean 不熟悉。

我们何时见过 @Component 及其衍生的3个注解 @Controller、@Service、@Repository 加在接口上面的?

自己测试新建个接口,上面加注解,然后找个 Controller 里 @Autowired 注入一下,项目立马会报错 NoSuchBeanDefinitionException 。

2. 切入源码

切入点

既然使用注解 @MapperScan 就好使,那么我们就从这个点切入源码看一下,先找出源码中何处用了此注解,非常幸运的是,只有一处用到了此注解 :MapperScannerRegistrar.registerBeanDefinitions() 。

并且从类名和方法名就可以很清楚的看出这个类的功能是扫描 Mapper 并注册,方法的功能就是注册 BeanDefinitions 到 Spring 中。方法的源码我就不贴了,很容易看出来是创建一个扫描器 ClassPathMapperScanner ,设置好一系列属性比如 Spring 的注册表之后,执行 doScan() 方法去扫描 @MapperScan 提供的包。

doScan() 扫描资源,转换为 BeanDefinition

doScan() 方法也很简单,就是两步:

  1. 调用父类 ClassPathBeanDefinitionScanner 的doScan()方法,也就是 Spring 扫描BeanDefinition 的方法。过程不是很重要,我们需要知道这个扫描方法的一个关键就是
1
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);

在其中对所有的候选者使用 isCandidateComponent() 方法判断是否为符合要求的 BeanDefinition。

1
2
3
4
5
6
7
8
9
10
for (TypeFilter tf : this.excludeFilters) {
if (tf.match(metadataReader, this.metadataReaderFactory)) {
return false;
}
}
for (TypeFilter tf : this.includeFilters) {
if (tf.match(metadataReader, this.metadataReaderFactory)) {
return isConditionMatch(metadataReader);
}
}

这有两组过滤器来过滤扫描到的资源。Spring 默认的过滤器是排除掉抽象类/接口的。而MyBatis 的扫描器重新注册了过滤器,默认对接口放行。

其实还有一些其它的过滤要求,但是不影响我们本问题的探究,所以不深入解读了。

源码读到这里,我们先找到了本文的第二个问题的答案。也就是 Spring 会忽略掉接口上面的注解,不会添加它进入 BeanDefiniiton ,也就难怪测试的时候会抛出 NoSuchBeanDefinitionException 的异常了。而 MyBatis 则会把这些接口拉过来注册BD 。

对 BeanDefinition 的加工

读到这里我们可能有了更大的疑问,拿接口注册 BeanDefinition ,那获取 Bean 的时候如何去实例化这个对象啊?接口可是不能实例化出对象的啊,而且我们也没有做实现。

原来是 MyBatis 的扫描器在调用完父类的扫描方法后,对 BeanDefinition 进行了加工 processBeanDefinitions() 。其中最关键的两行代码是

1
2
definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); 
definition.setBeanClass(this.mapperFactoryBean.getClass());

第一行,我们发现把这个接口的类名塞到了构造器参数中

小彩蛋,这里塞的是 String ,而我们的构造器参数其实要的是 Class 。但是 Spring 的 ConstructorResolver.autowireConstructor 中用到了 Object[] argsToUse 去做了个转换 。

第二行,beanDefinition 的 BeanClass 被设置成了 MapperFactoryBean !

熟悉 Spring 和 MyBatis 的读者肯定一下就明白了,就是这个地方进行了”偷梁换柱”!

1
2
@Autowired
private UserDao userDao;

还是拿 UserDao 为例,我们向 Spring 容器说 “给我来个 UserDao 的实例”,而 Spring 根据注册时候的 BeanDefinition ,去工厂( MapperFactoryBean )里面扔了个 UserDao.class 的参数进去,工厂的 getObject() 方法给我们返回了它制造的 userDao 。

就这样,我们没有去写实现类,轻轻松松拿到了我们需要的 userDao 。

至于 MapperFactoryBean 里做了什么返回了 userDao 出来?其实就是它的 getObject 方法返回的是 DefaultSqlSession.getMapper(Class type)方法,返回的是 MapperProxy 代理的类,而这个代理类的 invoke 方法并不像我们平时见到的代理中的 invoke 方法一样调用原始目标的 method.invoke ,而是去找 MapperMethod 执行了。

收获

这次的源码探究下来,收获的不仅仅是了解了 Dao 层 Bean 的注入,更是串起了我们最常用的 Spring 和 MyBatis ,换句话说,我们打通了从 Service 层到 Dao 层。

在以往 Debug 代码时看到的 MapperProxy,MapperMethod,我们清楚了这是从何而来,也对 MyBatis 中代理的巧妙运用更加熟悉。

参考文献

https://blog.csdn.net/java280580332/article/details/72123890

https://blog.csdn.net/mingtian625/article/details/47684271

评论和共享

阅读源码

初读 Spring

2017年9月开始阅读 Spring 源码,便在博客上记录自己的阅读笔记,阅读的过程真的是恨痛苦,一个月的时间两大章节还没读完,效果上也不明显。

后面几个月公司业务繁忙更是停下了读源码的节奏。

MyBatis

而12月入了一本《MyBatis 技术内幕》,介绍 MyBatis 的书,想从 MyBatis 入手,并且带着自己的小问题去研究 MyBatis 的源码,去探究一下我用 JSONObject 为什么就可以替代 JavaBean 。

花了一个月的时间便读完了第一遍书,也大致解答了自己的疑惑。发现研究起 MyBatis 确实是轻松很多,总结一下有以下方面的原因:

  • MyBatis 源码量小很多,层次结构清晰,功能明确,确实比 Spring 要简单很多
  • 作者划分章节层次合适
  • 学会了 Debug 源码
  • 拿起了实体书…比9月看电子书时确实方便很多

一本书走完一遍,感觉清楚了很多,对于 MyBatis 剩下的任务,就是再读一遍,特别是带着问题再读,比如去研究它的缓存、配置、反射。

再读 Spring

1月再回过头继续学习 Spring ,其实最大的转变就是,不再执着于见到一段代码就想一直钻到底弄清楚了,Spring 的层次太深,特别容易钻着钻着就把自己绕得不知道在哪了。因此,还是根据书本介绍,文档注释和函数/变量名称大概了解函数的作用先,待刷完一遍之后再回过头二刷再追求搞明白吧。

转变

博客

关于博客的记录,也要做一做转变了。起初写得东西只是笔记,渐渐加入了自己的理解,但是目前来看质量还是不够,有一个很重要的原因就是自己对一些还没有深刻的认识。

前阵子想独立钻研一波 Spring 的 autowiredByType,就匆匆忙忙开了一文,结果读着读着才发现这坑深不见底,实在不该在第一遍时就去碰…

因此,计划博客向更有营养的方向发展,尽量让博客能记录、传播一些能提升自己和其他读者认识的东西。像上一文自己动手实现解决循环依赖就是一个很好的主题,虽然文章写得不够好,技术含量也不够高,但是至少方向上来说确实能学到新东西。

学习

博客的更新频率将会降低,一方面是要自己先学透,提示博文质量,另一方面也是其实后面一段时间将会将重心放在找工作上。毕竟对于技术面试和以前的考试差不多,精读源码实在是性价比有点低,全面复习准备面试题效果更好。虽然我是不喜欢准备面试题的,但没有办法,下一份工作很关键,只有找到稳定的,能追求技术的团队,才能提供安心的钻研技术的环境。

评论和共享

从网上看到一篇博文 徒手撸框架–实现IoC ,写得很棒,作者抛开了 Spring 源码中复杂的校验,拓展等功能,实现了一个极简的 IoC 框架,让 Spring 源码初学者可以清楚的看到 IOC 的实现流程。

本文就借其框架,略加改造,再次介绍一下 Spring 是如何处理循环依赖的。

了解本项目核心代码需要先参考原作者的博文 徒手撸框架–实现IoC

循环依赖

其实很好理解,A 类依赖 B,B 又依赖 A。

说具体点就是 ,我们要 getBean(“a”), A 在实例化时需要为类型为 B 的成员变量赋值,因此去 getBean(“b”),而 getBean(“b”) 的时候又需要为其类型为A 的成员变量赋值,此时又会回过头去实例化 A ,导致无限循环。

用代码展示就是

1
2
3
4
5
6
7
8
public class A {
@AutoWired
private B b;
}
public class B {
@AutoWired
private A a;
}

代码改造

最主要的代码改造在于 BeanFactoryImpl 内, 添加了成员变量

1
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);

用于缓存正在创建中的,提前暴露出来的单例 bean。

在获取 bean 时,会在创建之前先从此 Map 中尝试获取,而这就是解决循环依赖的关键。

以上面的例子来说,就是一开始 getBean(“a”) 时,将未完成的 a 放入缓存,getBean(“b”) 时,需要去获取 a ,会从缓存中获取,而不是再去实例化 a。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Override
public Object getBean(String name) throws Exception {
//查找对象是否已经实例化过
Object bean = beanMap.get(name);
if (bean != null) {
return bean;
}
Object earlyBean = earlySingletonObjects.get(name);
if (earlyBean != null) {
System.out.println("循环依赖,提前返回尚未加载完成的bean:" + name);
return earlyBean;
}
//如果没有实例化,那就需要调用createBean来创建对象
BeanDefinition beanDefinition = beanDefineMap.get(name);
bean = createBean(beanDefinition);
if (bean != null) {
earlySingletonObjects.put(name, bean);
//对象创建成功以后,注入对象需要的参数
populatebean(bean, beanDefinition);
//再吧对象存入Map中方便下次使用。
beanMap.put(name, bean);
//从早期单例Map中移除
earlySingletonObjects.remove(name);
}
//结束返回
return bean;
}

Q & A

Q: 构造器循环依赖为什么无法解决?

A: 从上面代码可以看出,需要在 createBean 之后,才能将其放入缓存,而构造过程是在 createBean 之内的,此时尚未构造好一个基本的 bean ,拿什么放入缓存呢?

心得

上面只贴了 getBean 的代码,仅仅修改了原作者不到 10 行代码,其实在修改原框架,实现我们要的功能时不止这么多,包括调整对 json 的解析,对 bean 的填充等。

感受到 Spring 框架真的是很复杂很全面,这复杂程度靠说是说不清楚的,也不是翻一遍书看看源码就能明白的。而且看源码其实还是似懂非懂,中间的细节迷迷糊糊就可能跳过去了。

在追随 Spring 脚步,复现其代码的时候,才更深刻的理解其中很多操作,很多类的作用。比如说 BeanDefinition, BeanWrapper , PropertyDescriptor 这些类在我想要实现一些功能的时候才能体会到 Spring 创造它们的重要性。

评论和共享

前文

源码解析

入参说明

  • includeNonSingletons:是否包括非单例的 bean,比如 prototype scope
  • allowEagerInit:为了这个检查(找出所有匹配类型的 beanName),是否初始化 lazy-init 单例和由 FactoryBeans 创建的对象。此处我们传入的值为 true。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

public static String[] beanNamesForTypeIncludingAncestors(
ListableBeanFactory lbf, Class<?> type, boolean includeNonSingletons, boolean allowEagerInit) {

Assert.notNull(lbf, "ListableBeanFactory must not be null");
//方法主干还是在这行 getBeanNamesForType
String[] result = lbf.getBeanNamesForType(type, includeNonSingletons, allowEagerInit);
//下面的内容就是从 bf 的 parent 中找,
if (lbf instanceof HierarchicalBeanFactory) {
HierarchicalBeanFactory hbf = (HierarchicalBeanFactory) lbf;
if (hbf.getParentBeanFactory() instanceof ListableBeanFactory) {
//此处以 parent 再来调此方法,合并结果。
String[] parentResult = beanNamesForTypeIncludingAncestors(
(ListableBeanFactory) hbf.getParentBeanFactory(), type, includeNonSingletons, allowEagerInit);
List<String> resultList = new ArrayList<String>();
resultList.addAll(Arrays.asList(result));
for (String beanName : parentResult) {
if (!resultList.contains(beanName) && !hbf.containsLocalBean(beanName)) {
resultList.add(beanName);
}
}
result = StringUtils.toStringArray(resultList);
}
}
return result;
}

看来我们还需要继续深入到 getBeanNamesForType中去一探究竟。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public String[] getBeanNamesForType(Class<?> type, boolean includeNonSingletons, boolean allowEagerInit) {
//configurationFrozen:判断所有 bean 的 定义元数据是否可以被缓存
//如果不能缓存 或没type 或不允许急切初始化,则直接查 doGetBeanNamesForType
if (!isConfigurationFrozen() || type == null || !allowEagerInit) {
return doGetBeanNamesForType(ResolvableType.forRawClass(type), includeNonSingletons, allowEagerInit);
}
//否则先查缓存,没有缓存的话再查 doGetBeanNamesForType 并塞入缓存
Map<Class<?>, String[]> cache =
(includeNonSingletons ? this.allBeanNamesByType : this.singletonBeanNamesByType);
String[] resolvedBeanNames = cache.get(type);
if (resolvedBeanNames != null) {
return resolvedBeanNames;
}
resolvedBeanNames = doGetBeanNamesForType(ResolvableType.forRawClass(type), includeNonSingletons, true);
//判断是否缓存安全:依据我们目标class 和当前beanFactory的classLoader是否一致
if (ClassUtils.isCacheSafe(type, getBeanClassLoader())) {
cache.put(type, resolvedBeanNames);
}
return resolvedBeanNames;
}

再深入一层到 doGetBeanNamesForType ,其中逻辑外层是遍历所有的 beanName, 对于不是别名的进行处理,处理过程如下(省略了 try-catch ):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//首先获取这个beanName对应的mbd,它的相关定义配置信息
RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
// Only check bean definition if it is complete.
// 不是抽象的
// 并且 允许急切初始化 或 (此bean不需要急切初始化 且(有beanClass 或 不是lazyInit 或 允许急切的类加载,即使是懒惰的初始化bean))
if (!mbd.isAbstract() && (allowEagerInit ||
((mbd.hasBeanClass() || !mbd.isLazyInit() || isAllowEagerClassLoading())) &&
!requiresEagerInitForType(mbd.getFactoryBeanName()))) {
// In case of FactoryBean, match object created by FactoryBean.
//判断是否 FactoryBean
boolean isFactoryBean = isFactoryBean(beanName, mbd);
BeanDefinitionHolder dbd = mbd.getDecoratedDefinition();
//接下来又是一段非常长的逻辑判断,判断是否匹配
boolean matchFound =
(allowEagerInit || !isFactoryBean ||
(dbd != null && !mbd.isLazyInit()) || containsSingleton(beanName)) &&
(includeNonSingletons ||
(dbd != null ? mbd.isSingleton() : isSingleton(beanName))) &&
isTypeMatch(beanName, type);
if (!matchFound && isFactoryBean) {
// In case of FactoryBean, try to match FactoryBean instance itself next.
//如果不匹配,还要试试匹配FactoryBean本身,因为说不好要的就是这个FactoryBean呢
beanName = FACTORY_BEAN_PREFIX + beanName;
matchFound = (includeNonSingletons || mbd.isSingleton()) && isTypeMatch(beanName, type);
}
if (matchFound) {
result.add(beanName);
}
}

//检查一遍手动的单例集合
//对于 FactoryBean ,如果匹配到它的getObject()满足,就不会继续去匹配它本身
for (String beanName : this.manualSingletonNames) {
// In case of FactoryBean, match object created by FactoryBean.
if (isFactoryBean(beanName)) {
if ((includeNonSingletons || isSingleton(beanName)) && isTypeMatch(beanName, type)) {
result.add(beanName);
// Match found for this bean: do not match FactoryBean itself anymore.
continue;
}
// In case of FactoryBean, try to match FactoryBean itself next.
beanName = FACTORY_BEAN_PREFIX + beanName;
}
// Match raw bean instance (might be raw FactoryBean).
if (isTypeMatch(beanName, type)) {
result.add(beanName);
}
}

}

前一段的超长的逻辑判断看得人头疼,但是先看下半段的遍历,突然就找到了最关键判断类型匹配的函数 isTypeMatch,从名字就看出来,这应该就是判断类型匹配的地方啦。

然而点进去一看,居然是长达 100 行的函数。

isTypeMatch

首先从单例中查找,匹配这个单例 bean 的类型和我们目标的类型,其中对 factoryBean 的处理也比较简单,就不再贴代码了。对于注册的 null instance ,也返回 false。

再从父工厂找,递归 isTypeMatch 。

再就是复杂的查找了。

先定义个 typesToMatch ,包括了目标类型和 FactoryBean .

然后我们再继续看代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// Check decorated bean definition, if any: We assume it'll be easier
// to determine the decorated bean's type than the proxy's type.
// 先检查 bean 的装饰definition。
BeanDefinitionHolder dbd = mbd.getDecoratedDefinition();
if (dbd != null && !BeanFactoryUtils.isFactoryDereference(name)) {
RootBeanDefinition tbd = getMergedBeanDefinition(dbd.getBeanName(), dbd.getBeanDefinition(), mbd);
Class<?> targetClass = predictBeanType(dbd.getBeanName(), tbd, typesToMatch);
if (targetClass != null && !FactoryBean.class.isAssignableFrom(targetClass)) {
return typeToMatch.isAssignableFrom(targetClass);
}
}
// 注意此处predictBeanType返回了很关键的Class ,我们后面再详细分析此方法。
Class<?> beanType = predictBeanType(beanName, mbd, typesToMatch);
if (beanType == null) {
return false;
}

// Check bean class whether we're dealing with a FactoryBean.
//接下来就是处理 FactoryBean
if (FactoryBean.class.isAssignableFrom(beanType)) {
if (!BeanFactoryUtils.isFactoryDereference(name)) {
// If it's a FactoryBean, we want to look at what it creates, not the factory class.
beanType = getTypeForFactoryBean(beanName, mbd);
if (beanType == null) {
return false;
}
}
}
else if (BeanFactoryUtils.isFactoryDereference(name)) {
// Special case: A SmartInstantiationAwareBeanPostProcessor returned a non-FactoryBean
// type but we nevertheless are being asked to dereference a FactoryBean...
// Let's check the original bean class and proceed with it if it is a FactoryBean.
beanType = predictBeanType(beanName, mbd, FactoryBean.class);
if (beanType == null || !FactoryBean.class.isAssignableFrom(beanType)) {
return false;
}
}
//对其 resolvableType 进行处理
ResolvableType resolvableType = mbd.targetType;
if (resolvableType == null) {
resolvableType = mbd.factoryMethodReturnType;
}
if (resolvableType != null && resolvableType.resolve() == beanType) {
return typeToMatch.isAssignableFrom(resolvableType);
}
//如果以上都没有处理掉的话, 则判读typeToMatch和 beanType
return typeToMatch.isAssignableFrom(beanType);

总结:

目前很遗憾 isTypeMatch 往下还有很复杂的逻辑暂时不能看懂,但是从外层的逻辑大致知道寻找所有匹配的beanName 的方法非常的“复杂粗暴”。遍历 beanDefinitionNames 已定义的所有 bean,即使是一个小型的项目也有近200个 bean 需要遍历,并且这数百个 beanName 还要遍历非常多次。

只能说根据 type 寻找 bean 实在是比根据 name 复杂了太多太多,从源码看真是深坑,理解了为什么作者直接忽略了这部分…应该是第二遍或第三遍阅读 Spring 源码时才能理解。

评论和共享

前言

Spring 装配 bean 有两种类型:autowireByNameautowireByType

autowireByName 通过名称查找很直接,就是我们一直在学的 getBean() 。

autowireByType 根据类型查找相比起来就要复杂一些了,《 Spring 源码深度解析 》 中有介绍过的部分我就不再重复贴了,但是书中遗漏了一处重要的部分—— findAutowireCandidates 查找所有合适的 bean,还有一处新版本 Spring 中升级的部分——如果只需要一个但是找出多个 bean 该怎么处理,今天我们先来学习第一部分。

正文

本函数要做什么

1
2
3
4
5
6
@RestController
@RequestMapping("/article")
public class ArticleController {
@Autowired
private ArticleService articleService;
}

在初始化 ArticleController 的过程中,我们要为其装配 ArticleService 。

忽略掉外面代码一层一层的包裹之后,我们走到 DefaultListableBeanFactory.findAutowireCandidates 这个函数中,要寻找合适的候选 bean 。由于可能会找到多个,因此返回结果是候选 bean 的名称和其对应实例构成的 Map 。

源码解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
//三个参数的含义依次是 
// 正在解析的 beanName, 本例中即 "articleController"
// 需要装配的 bean 类型, 本例中即 ArticleService.class
// 对当前依赖关系的解析类,记录了 ArticleController 和 ArticleService 的依赖关系
protected Map<String, Object> findAutowireCandidates(String beanName,
Class<?> requiredType,
DependencyDescriptor descriptor) {

//第一步就是查找出所有符合类型的 beanName 。
//似乎第一句就干完全部逻辑了???稍后我们再详细分析这个方法。
String[] candidateNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
this, requiredType, true, descriptor.isEager());
Map<String, Object> result = new LinkedHashMap<String, Object>(candidateNames.length);
/**
* resolvableDependencies 记录了 依赖类型--具体装配值 的映射
* 遍历 resolvableDependencies。如果该类型是我们需要的类型(ArticleService),
*
*/
for (Class<?> autowiringType : this.resolvableDependencies.keySet()) {
if (autowiringType.isAssignableFrom(requiredType)) {
Object autowiringValue = this.resolvableDependencies.get(autowiringType);
//key值是我们需要的类型,但value值未必。
//value可能是ObjectFactory,就得调用它的 getObject() 来获取真正的bean.
autowiringValue = AutowireUtils.resolveAutowiringValue(autowiringValue, requiredType);
if (requiredType.isInstance(autowiringValue)) {
//如果类型匹配,则塞入result
result.put(ObjectUtils.identityToString(autowiringValue), autowiringValue);
break;
}
}
}
for (String candidate : candidateNames) {
//如果不是自己依赖自己 , 并且符合装配候选,就塞入result。
//何为符合装配候选(isAutowireCandidate)呢?稍后我们再详细分析。
if (!isSelfReference(beanName, candidate) && isAutowireCandidate(candidate, descriptor)) {
addCandidateEntry(result, candidate, descriptor, requiredType);
}
}
if (result.isEmpty() && !indicatesMultipleBeans(requiredType)) {
// Consider fallback matches if the first pass failed to find anything...
//如果之前一轮都没找到,则考虑回退匹配,什么是回退匹配?稍后再分析。
DependencyDescriptor fallbackDescriptor = descriptor.forFallbackMatch();
for (String candidate : candidateNames) {
if (!isSelfReference(beanName, candidate) && isAutowireCandidate(candidate, fallbackDescriptor)) {
//再执行一遍上面的方法。如果不是自己依赖自己,并且符合装配候选,就塞入result。
addCandidateEntry(result, candidate, descriptor, requiredType);
}
}
if (result.isEmpty()) {
// Consider self references as a final pass...
// but in the case of a dependency collection, not the very same bean itself.
// 如果依然没找到结果,那么满足以下条件的也是我们的目标。
// 1.是自引用
// 2.依赖不是多元素依赖 或者 bean名和候选者名字不相等(这里就避免了自引用导致无限循环)
// 3.候选者符合回退匹配之后的装配候选
for (String candidate : candidateNames) {
if (isSelfReference(beanName, candidate) &&
(!(descriptor instanceof MultiElementDescriptor) || !beanName.equals(candidate)) &&
isAutowireCandidate(candidate, fallbackDescriptor)) {
addCandidateEntry(result, candidate, descriptor, requiredType);
}
}
}
}
return result;
}

看完主干自然还是有点迷糊,我们留下了三个问题要继续研究:

评论和共享

Spring vs MyBatis

Spring 和 MyBatis 中都有 BeanWrapper , Spring 中为接口, 实现类为 BeanWrapperImpl , 为了方便后面区分,本文用 SB 指代 Spring 的 BeanWrapperImpl ,用 MB 指代 MyBatis 中的 BeanWrapper。

功能

BeanWrapper 都属于各自框架的反射工具箱的重要组成部分。都是创建实例并且为其属性赋值的。以 SB 为例,下面的代码应该很容易看明白它的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
BeanWrapper company = BeanWrapperImpl(new Company());
// setting the company name..
company.setPropertyValue("name", "Some Company Inc.");

// ... can also be done like this:
PropertyValue value = new PropertyValue("name", "Some Company Inc.");
company.setPropertyValue(value);

// ok, let's create the director and tie it to the company:
BeanWrapper jim = BeanWrapperImpl(new Employee());
jim.setPropertyValue("name", "Jim Stravinsky");
company.setPropertyValue("managingDirector", jim.getWrappedInstance());

// retrieving the salary of the managingDirector through the company
Float salary = (Float) company.getPropertyValue("managingDirector.salary");

共同点

  1. 核心功能功能基本相同
  2. 对要生成的类的要求规范也相同,有 get set 方法。
  3. 它们的底层也没有什么秘密,归根到底都是使用 java.lang.reflect 包下的 Constructor , Method 等等工具。

不同点

可能是我刚开始研究 SB 的原因,感觉 SB 源码更加复杂,结构也没那么清楚,关联了向下层级的工具类,很长很长一段的源码。 这篇博文 分析了其中一部分。

得益于已经学习过结构更为清晰的 MB,尽管 SB 源码复杂,但是读的时候不再会恐惧,因为简略的Debug 一遍就知道它底层还是调 getter/setter 的反射。再复杂的结构也离不开这最终的方法。

已目前对 SB 粗浅的了解来说,感受到最大的区别就是在工具箱中的结构地位不同

MB 属于BaseWrapper 的子类之一,同级别的还有 MapWrapper ,上级还有 CollectionWrapper 。

而 SB 就已经是在创建 Bean 时直接使用到的接口了。

解析嵌套参数名 (比如 user.name / address.city.mailcode),类型转换这些事情,SB 都能处理完。而 MB 都是先要使用其它工具类处理,比如依靠 PropertyTokenizer 。

总而言之,SB 就是对外的一个大接口,包含很多功能,MB 则是MyBatis 反射工具箱内的一个小的工具实现。

评论和共享

此方法交给了 AbstractBeanFactory 的子类 AbstractAutowireCapableBeanFactory 去实现。

并且不管这个bean是单例还是 prototype 还是其它 scope ,最终都是会走到此处,只是前后的一些验证、处理有区别。比如单例的就要先去缓存中获取,prototype 就不需要。

源码阅读到这里,我们已经习惯了一层一层剥。createBean 依然还是没有直接地把 bean 创建出来(当然我所期望的看到创建 bean 就是看到它的反射源码为止)。

createBean 的大致步骤为:

  1. 根据 RootBeanDefinition 来获取要创建 bean 的 class 。这 class 还有可能为 null。
  2. prepareMethodOverrides 。准备 override 方法,对 override 属性进行验证。
  3. 给后处理器一个机会来返回代理,替代真正的 bean.
  4. doCreateBean 创建真正的 bean 实例。

prepareMethodOverrides

首先去温习一遍 lookup-method 和 replace-method 吧。博文

其实就是通过配置把原本 bean 中的某个方法给替代掉。

此处我们先只是确认一遍指定的替代方法存在于要生成的 bean 中。

顺带看一看这个方法有没有重载overload),做个标记。

resolveBeforeInstantiation

经过一波预处理器InstantiationAwareBeanPostProcessor ,如果生产出了 bean,再经过一波后处理器。

一旦生产出 bean,则立即将此 bean 返回。

此处就是留下了一个拓展点,经过此方法之后,bean可能已经不是我们认为的 bean 了,而可能已经变成了一个经过处理的代理 bean 。

循环依赖

构造器循环依赖

如果是 prototype,无法解决,只能抛错。

代码在 AbstractBeanFactory : 256 doGetBean()

当创建 bean 时,首先去“当前创建 bean 池”查找是否当前 bean 正在创建,如果发现存在,则表示循环依赖了。抛出 BeanCurrentlyInCreationExcetion 。

当前创建 bean 池:

1
2
private final ThreadLocal<Object> prototypesCurrentlyInCreation =
new NamedThreadLocal<Object>("Prototype beans currently in creation");

setter 循环依赖

只能解决单例的情况。

在创建单例 bean 时,提前暴露刚完成构造器但未完成其他步骤(如 setter 注入)的 bean 。

通过提前暴露这个单例工厂方法,从而使其他 bean 能够引用到此 bean。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
if (earlySingletonExposure) {
if (logger.isDebugEnabled()) {
logger.debug("Eagerly caching bean '" + beanName +
"' to allow for resolving potential circular references");
}
addSingletonFactory(beanName, new ObjectFactory<Object>() {
@Override
public Object getObject() throws BeansException {
//注意这里返回的是 早期引用
return getEarlyBeanReference(beanName, mbd, bean);
}
});
}

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(singletonFactory, "Singleton factory must not be null");
synchronized (this.singletonObjects) {
if (!this.singletonObjects.containsKey(beanName)) {
//单例工厂注册
this.singletonFactories.put(beanName, singletonFactory);
//早期单例移出
this.earlySingletonObjects.remove(beanName);
//注册单例加入
this.registeredSingletons.add(beanName);
}
}
}

简单地记一下这里解决步骤:

testA 先创建,并且暴露一个工厂出去,进行 setter 注入 testB.

testB 创建,并且暴露一个工厂,进行 setter 注入 testA.

在这里想用 testA 时,由于发现提前暴露的工厂,从而在此处走了另一条路,使用此工厂来创建 testA ,在此处解决了循环问题。

再返回回去继续完成 testA 的 setter 注入。

评论和共享

作者的图片

heeexy

世上是不是就没有你不认识的字了?


JAVA


南京