0%

前言

用了Spring这么多年,以为我对循环依赖的问题已经了如指掌,但是现实还是给我了一巴掌。

最近线上服务报了错误:

Bean with name xx has been injected into other beans [ x ] in its raw version as part of a circular reference,

but has eventually been wrapped. This means that said other beans do not use the final version of the bean.

This is often the result of over-eager type matching - consider using ‘getBeanNamesOfType’ with the ‘allowEagerInit’ flag turned off, for example.

Exception是BeanCurrentlyInCreationException,看起来是个循环依赖的问题,但是日志的报错是我从来没见过的。

正常的循环依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class A {
private B b;

public A(B b) {
this.b = b;
}
}


@Service
public class B {
private A a;

public B(A a) {
this.a = a;
}
}

如上,我们创建了两个Service,并且通过构造函数的方式注入。

启动时,Spring会报错:

Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name ‘a’: Requested bean is currently in creation: Is there an unresolvable circular reference?

并且还会贴心的会你画个图:

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
| a defined in file [A.class]
↑ ↓
| b defined in file [B.class]
└─────┘

这种也是我理解中的循环依赖问题,解决方案也是比较简单,我们换成set方法注入,或者成员变量注入就行。

复现上文的异常

上文的异常,我一开始以为是AOP导致的问题,于是在A上加了个Aop的方法,发现Aop能够很好的解决这种循环依赖。

解决方案是三级缓存:

参考文章: https://segmentfault.com/a/1190000039134606

那我就疑惑了,到底是什么情况导致的呢?

后来搜到了文章:https://segmentfault.com/a/1190000018835760

发现@Async可以复现,于是我修改了代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Service
public class A {
private B b;

@Autowired
public void setB(B b) {
this.b = b;
}

@Async
public void doAsync() {
System.out.println("hello");
}
}

@Service
public class B {
private A a;

@Autowired
public void setA(A a) {
this.a = a;
}
}

发现确实可以复现了,这就暴露出我的知识盲区了,难道@Async不是通过Aop去解决的吗?

为什么@Aspect注解切的方法,不会报循环依赖,但是@Async的方法会呢?

带着疑惑,我又尝试了@Transectional会不会导致循环依赖问题,发现并不会。

所以,为啥@Async如此特殊呢?

对比

于是我得对比,@Async和@Transectional在实现上的区别

为此,我们把A的方法声明称这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public class A {
private B b;

@Autowired
public void setB(B b) {
this.b = b;
}

@Transactional
@Async
public void doAsync() {
System.out.println("hello");
}
}

同时配上一个手写的Aspect:

1
2
3
4
5
6
7
8
9
@Aspect
@Component
public class AspectA {

@Before("execution(public A.doAsync())")
public void beforeA() {
System.out.println("beforeA 执行");
}
}

在DefaultAdvisorChainFactory的getInterceptorsAndDynamicInterceptionAdvice方法中

我们可以查到这个方法所有的Advisor。

可以看到一共有三个

  1. ExposeInvocationInterceptor:先不管,和我们的业务无关
  2. BeanFactoryTransactionAttributeSourceAdvisor:就是我们@Transactional的相关Aop
  3. AspectA:自定义的Aop

可以发现,并没有@Async相关的Aop代码。

而通过对DefaultAdvisorChainFactory的调用链,可以分析到,这里的执行,对原来类的Aop织入,其实在

1
2
doCreateBean#
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, 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) {
Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {
//这里拿到的exposedObject和Bean其实是一样的,不会抛出异常。
if (exposedObject == bean) {
exposedObject = earlySingletonReference;
}
else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
String[] dependentBeans = getDependentBeans(beanName);
Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
for (String dependentBean : dependentBeans) {
if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
actualDependentBeans.add(dependentBean);
}
}
if (!actualDependentBeans.isEmpty()) {
throw new BeanCurrentlyInCreationException(beanName,
"Bean with name '" + beanName + "' has been injected into other beans [" +
StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
"] in its raw version as part of a circular reference, but has eventually been " +
"wrapped. This means that said other beans do not use the final version of the " +
"bean. This is often the result of over-eager type matching - consider using " +
"'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");
}
}
}
}

那为什么加了@Async就会不一样呢?

通过上文,我们知道了@Async的逻辑和传统的Aop逻辑不太一样。

@Async是通过AsyncAnnotationBeanPostProcessor去织入自己的逻辑。

这个BeanPostProcessor在applyBeanPostProcessorsAfterInitialization方法中被调用

导致生成的是一个新的代理Bean,和原来的Bean就会不一样,也就是走到了抛出异常的逻辑。

总结

还是不能想当然,Aop是一个广泛的概念,但是在Spring中,其实还是冗余了不同的实现。

并不是所有加了Annotation的实现,都是一样的。

有可能是Aspect的实现,也有可能是类似BeanPostProcessor的实现。

需要看源码具体原因具体分析。

A Generational Mostly-concurrent Garbage Collector
(题目不知道咋翻,算了),论文发布在2000年

注:本文讲的不完全是CMS垃圾收集器,其实讲的是并发垃圾收集器的思路。

而Java中的并发垃圾收集器包含CMS和G1。

Read more »

一. 前言

Disruptor几乎是每个Java开发绕不过去的坎,其实我想学习这个框架很久了,之前打开看了几次,但是有点复杂就放弃了。

这一次看到了Mpsc,心里在构思多生产者多消费者的队列怎么怎么做,自然就想到了RingBuffer。

有了上文的基础,下面我们就Disruptor来看看多生产者多消费者是怎么实现的。

注意:这个文章并不是特别的分析Disruptor是怎么实现高性能的,诸如网上说的那些伪共享之类,

就是带大家看看源码实现。

Read more »

前言

前面我们聊完了Mpsc,在提一下,Mpsc主要是针对的单消费者多生产者的情况。

对于消费者而言,因为只有一个消费者,所以不需要任何同步。

对于生产者而言,为了防止多线程下会出现问题,所以使用CAS操作。

但是上一篇文章中Mpsc使用的是链表的结构,不加以控制容易OOM。

为了避免这个问题,我们可以使用数组来作为底层存储。

原理其实和这边文章的RingBuffer讲的类似。

这里的RingBuffer其实就是Disruptor的实现,不过我单独抽成了一个文章来讲Disruptor的源码。

这里就划一些示意图,解释RingBuffer的原理。

Read more »

前言

其实一开始没有接触过这个,但是看Netty源码的时候,发现Netty的对象池使用了MpscQueue,感觉还挺有意思的。

MpscQueue主要针对的是单消费者,多生产者的情况,实现上是LockFree的,这里的Lock Free,一般都是指用了CAS解决并发问题。

Read more »

Manifest文件,简单的看就是当前VersionEdit的持久化信息,其中包含了:

  1. Comparator:全局的比较方法
  2. LogNumber:下一个MemTable的WAL日志文件的FileNumber
  3. PreviousLogNumber:已经被废弃,之前版本有用到
  4. NextFileNumber:下一个File的数字
  5. LastSequence:最新的Seq
  6. Compact_Pointer:在Compact一章中统一讲。
  7. Deteted_Files:相对于上一个Version,删除的文件
  8. New_Files:相对于上一个Version,新增的文件

以上内容都在VersionEditTag中进行读写。

其中Manifest文件只会存在一个,但是名字中的FileNumber不是一定的,在数据目录下,文件名可能是

MANIFEST-000540。个人猜想可能是版本问题导致的。

初始化:

在DbIMPL进行初始化时,会创建VersionSet,在VersionSet的构造函数中,调用了initializeIfNeeded对Manifest进行了初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void initializeIfNeeded() {
File currentFile = new File(databaseDir, Filename.currentFileName()); // 1
if (!currentFile.exists()) {
VersionEdit edit = new VersionEdit(); // 2
edit.setComparatorName(internalKeyComparator.name());
edit.setLogNumber(prevLogNumber);
edit.setNextFileNumber(nextFileNumber.get());
edit.setLastSequenceNumber(lastSequence);

LogWriter log = Logs.createLogWriter(new File(databaseDir, Filename.descriptorFileName(manifestFileNumber)), manifestFileNumber); // 3
try {
writeSnapshot(log); // 4
log.addRecord(edit.encode(), false); // 5
}
finally {
log.close();
}
Filename.setCurrentFile(databaseDir, log.getFileNumber()); // 5
}
}
  1. 找出名为”CURRENT”文件,前面提到这个文件的内容就是Manifest文件的文件名。
  2. 这里CURRENT文件不存在,所以new一个VersionEdit,其中prevLogNumber是0,nextFileNumber是2,lastSeq也是0。
  3. 这里的manifestFileNumber文件是1,这里知道为什么nextFileNumber默认是2开始了吧,因为1是ManifestFile的初始化FileNumber
  4. 这个方法里,new了一个VersionEdit,基本上什么值也没设,这里不知道为什么要先把这个VersionEdit放进去。
  5. 把上面的VersionEdit写入到Manifest中
  6. 这里把上面的Manifest文件名,写入CURRENT文件,这个方法中用到了Temp文件。

每次Compact过后,就会调用VersionSet的logAndApply方法,把Edit的信息传入,加入到Manifest文件中。

这个方法下面会详细描述。

那么一个疑问就来了,每次Compact后都会往里面Append新的VersionEdit信息,那么这个文件不就会越来越大吗?就像Redis的Compact一样?是不是有什么机制,会导致创建新的Manifest文件,把当前的Version的快照放进去,然后丢弃旧的Manifest文件呢?

答案是有的:其中每次启动后,触发的第一次Compact,会导致旧的Manifest文件被丢弃,生成新的Manifest文件,把当前Version的快照信息放入。

这里其实有个问题的,只有每次重新启动后才会触发,如果一直在运行的话,其实不会触发重新清理的。

虽然在运行中并不会去读取这个Manifest文件,但是下次启动恢复Version信息时,需要从头到尾遍历这个文件,速度可能会很慢。

代码如下:

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
public void logAndApply(VersionEdit edit) {
//...
boolean createdNewManifest = false;
try {
if (descriptorLog == null) { // 1
edit.setNextFileNumber(nextFileNumber.get());
descriptorLog = Logs.createLogWriter(new File(databaseDir, Filename.descriptorFileName(manifestFileNumber)), manifestFileNumber); // 2
writeSnapshot(descriptorLog); // 3
createdNewManifest = true;
}

Slice record = edit.encode();
descriptorLog.addRecord(record, true);

if (createdNewManifest) {
Filename.setCurrentFile(databaseDir, descriptorLog.getFileNumber()); // 4
}
} catch (IOException e) {
//...
}
}


private void writeSnapshot(LogWriter log) {
// Save metadata
VersionEdit edit = new VersionEdit();
edit.setComparatorName(internalKeyComparator.name());

// Save compaction pointers
edit.setCompactPointers(compactPointers);

// Save files
edit.addFiles(current.getFiles());

Slice record = edit.encode();
log.addRecord(record, false);
}
  1. descriptorLog除了在这个方法,没有在其他地方被赋值过,所以第一次进来,肯定是null的
  2. 创建一个新的Manifest文件
  3. 调用writeSnapshot方法,生成VersionEdit,把当前所有的文件写入Manifest文件
  4. 设置CURRENT文件,指向新的Manifest文件

VersionSet的恢复:

前面提到过一个公式:OldVersion + VersionEdit = NewVersion

如果重新启动应用,要恢复到最新的Version,只要把Manifest文件中的VersionEdit全部apply一遍就行了。

方法在VersionSet::recover中,代码浅显易懂,这里就不展开了。

前言

SSTable就是把一个跳表放入一个文件,但是我们不仅仅是把KeyValue写入文件就结束了,还需要做一些其他的事:

  1. 写入KV
  2. SSTable的一些元信息,如最大Key,最小Key,KV的个数等。
  3. 为KV维护一定的索引,加速查找
  4. 进行一定比例的数据压缩
  5. 数据完整性校验

文件的大体格式如下:

文件的格式大致分为一个一个的Block

  1. Data Block存储的是数据,就是KV。
  2. MetaIndex Block
  3. Index Block
  4. Footer

其中在源码中,DataBlock是大头,2和3叫Footer。

MetaIndex在Java版本的实现中是个空Block。

这三个Block的底层实现都是BlockBuilder,BlockBuilder是个存储KV的格式。

个人感觉上其实BlockBuilder是为了DataBlock打造的,而IndexBlock只是恰好复用了一下。

所以下文将的DataBlock其实就是DataBlock的机制。

Read more »

前言

题目的Log指的是文件格式,并不特使某个FileType。

因为LOG文件和Manifest文件都是使用的Log格式的文件进行记录的。

这里主要讲解的是LogWriter和LogReader两个类。

格式镇楼:

log文件格式

Read more »

前言

在源码中,特地有个枚举是表示FileType

1
2
3
4
5
6
7
8
9
10
public enum FileType
{
LOG,
DB_LOCK,
TABLE,
DESCRIPTOR,
CURRENT,
TEMP,
INFO_LOG // Either the current one, or an old one
}

DB创建文件时将FileNumber加上特定的后缀作为文件名,FileNumber在内部是一个uint64_t类型,并且全局递增。不同类型的文件的拓展名不同,例如sstable文件是.sst,wal日志文件是.log。LevelDB有以下文件类型:

一. FileNumber

每个文件的文件名都是一个数字,文件类型用后缀区分,即使是不同的后缀,文件的FileNumer也不会重复。

所以FileNumber类似于数据库的主键一下,自增的进行分配。

在VersionSet的变量中:

1
private final AtomicLong nextFileNumber = new AtomicLong(2);

可以看到默认的FileNumber是从2开始的。

为什么不是从1开始,因为1默认是给第一个Manifest文件

如果是重启的应用:

FileNumber会被写到CURRENT文件中,在VersionSet::recover中读取CURRENT文件指向的Manifest文件时恢复出来。

对于recover方法,本质上是CURRENT和Manifest文件,下面再讲。

二. FileType

2.1 DB_LOCK文件

这个文件作为文件锁存在,文件名就叫LOCK,里面不会保存任何东西

2.2 TABLE文件

就是SSTable文件,以.sst结尾

2.3 LOG文件

类似于WAL文件,以.log结尾

注意,这里指的是文件类型,并不是文件格式。

项目中有个LogWriter类,这个生成的文件的文件格式相同,但是既可以作为LOG文件,又可以作为Manifest文件。

数据目录下只会有一个LOG文件,每次新启时,会将旧的删除,但是文件名中仍然带有FileNumber。

2.4 DESCRIPTOR

就是常说的Manifest文件,以MANIFEST-开头

每次Compact之后,都会产生当前Compact后SSTable文件的修改VersionEdit。

同时将VersionEdit文件的内容写入Append到Manifest文件。

2.5 CURRENT

文件名就叫CURRENT,里面的内容是当前的MANIFEST文件的文件名。

2.6 Temp

因为Log文件和Manifest文件都只有一个,在使用新的覆盖的时候,需要先创建一个Temp文件,然后再rename成真正的。

2.7 INFO_LOG

似乎没用到

一. 介绍

项目中一直在使用RateLimiter进行单机的限流,但是没有去了解他的运作原理,这里就简单记录下,为以后学习Sentinal做铺垫。

Read more »

前言

第一次参加这种类似黑客马拉松的比赛,感觉还是挺新奇的。

虽然成绩不咋好,但是毕竟第一次参加,还是记录一下。

Read more »

在小米有品的工作内容也算是和社交有点关系,会有类似微博的点赞,查看点赞列表的功能。
这个功能看起来简单,其实做起来一点都不容易。
为了避嫌,这里以微博为例,讲一讲自己的思考。
类似的,还有关注列表等。这里就简单思考点赞列表。

Read more »

前面讲了CPU的演进,提出了StoreBuffer和InvalidateQueue的设计,并且讲解了这两个设计会带来的问题。
解决这两个问题就是引入内存屏障:强制刷新StoreBuffer和InvalidateQueue。

这里详细讲讲x86机器上的内存屏障指令与其他隐式的含有内存屏障的指令。
然后再聊一聊JMM与内存屏障的对应关系。

Read more »