0%

前言

最近遇到一个很有意思的问题,如下代码在多线程运行下的问题与解决方案

1
2
3
4
5
6
7
8
9
10
11
public class Counter {
private static int count = 0;

public static void increment() {
count++;
}

public static int getCount() {
return count;
}
}

变量累加的过程

由 Java 的内存模型可知,变量的累加经历三个阶段:从主内存读取到工作内存 -> 工作内存中修改 -> 写回主内存

存在的问题

由上述三步可知,可能存在问题有三个:

  1. 读取的值是否为当前时刻的最新值
  2. 累加的前提值是否为当前时刻的最新值
  3. 写回主内存是否为当前时刻期望值

问题 1 ,一定是。因为变量存在于内存某一块区域,任何时刻的读取都是对该内存的读取。

问题 2,不一定是。经历了复制到工作内存的阶段,工作内存和主内存的值不能保证强一致。

问题 3,不一定是。因为累加的前提值不准确以及多线程同时写回的顺序问题,所以修改后的值与期望值可能不一致。

所以问题的关键是:如何保证工作内存读取的是最新值,如何保证写回主内存是顺序的。

解决

本地

本地分为两种:自旋尝试更新 & 顺序更新,分别对应 CAS 更新以及锁后顺序更新。

CAS 更新

CAS 更新分为两种,AtomicInteger 以及 LongAddr。

AtomicInteger 应该都很熟悉,利用 volatile 关键字保证变量的可见性,自旋使用 Unsafe 提供的原子指令保证更新的准确性。

LongAddr 则是使用 Cell 数组保存更新指令,后续通过 sum 方法提供当前的计算结果。

区别
AtomicInteger LongAddr
计算结果 实时 非实时
作用原理 时间换空间(针对单变量的自旋更新) 空间换时间(针对 Cell 数组的 CAS 更新)
冲突概率

重量级锁

重量级锁分为两种:synchronized 以及 ReentrantLock。

synchronized 通过 monitor enter & monitor exit 内存屏障实现锁的获取与释放。

ReentrantLock 通过 AQS 队列保证锁获取/释放的顺序。

区别
synchronized ReentrantLock
基本性质 Java 关键字 Java API
实现方式 内存屏障 AQS(底层 CAS 更新)
公平性控制 只支持非公平锁 公平锁/非公平锁
多条件绑定 单个条件绑定 多 Condition 绑定
锁是否可升级 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁 公平锁/非公平锁
使用复杂度 低,不用考虑锁获取/释放过程 高,需要手动获取/释放锁
使用灵活度 高,可支持超时获取锁、尝试获取锁,锁中断的操作

分布式

Redis increment 操作

针对单个 redis key 做递增的操作。因为 Redis 操作是单线程的,所以本质上利用了 Redis 顺序操作的特性。

ZooKeeper 顺序节点

针对每次操作新增一个临时节点。通过计算序号的大小得到最终的结果。

区别

Redis increment Zookeeper
计算结果 实时 非实时(事后计算结果)
作用原理 顺序更新 对节点进行计数
性能 低,涉及节点新增与清除

为什么有意思

之前对于该问题,最常用就是 CAS/锁(本地),Redis(分布式)。优点是强一致,缺点是可能的自旋等待带来的性能损耗。(时间换空间,强一致)

如果只要最终一致但提升性能,只能暂存中间结果最后累加,但是自己实现又涉及操作升级、扩容等管理,而 Striped64 子类则实现了上述功能。(空间换时间,最终一致)

之前没有将最终一致的可行性与该问题联系到一起,而这也是我觉得有意思的地方。系统设计的 trade-off 体现得尤其明显。

本文首发于cartoon的博客

转载请注明出处:https://cartoonyu.github.io

前言

偶然发现单元测试运行的比较慢,比较有意思,所以就深究了一下源码,解决了一下。

解决前后对比

解决前,单测本地运行 10+ 分钟。

解决后,单测本地运行 1 分钟。

项目概况

SpringBoot 版本:2.3.x,junit 版本:5.x。

项目结构很简单:接收上游请求查询数据库做业务逻辑。

单测同时使用 @InjectMocks + @Spy/@Mock 以及 @SpyBean/@MockBean 对下游依赖进行 mock。

运行时表现

现象

单元测试逻辑运行很快,运行前Spring 容器不断被启动,数据库连接 / Bean 被重复加载。

而在普通业务项目中,运行时数据库 / Bean 的信息是确定的,重复加载并没有意义。

重复加载的 demo

所有单测都继承 BaseTest 基类,使用集成测试的方式运行所有单测。

业务代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class MockOuterService {
@Resource
private MockService mockService;

public String mockString(){ return mockService.mockString();}
}

public interface MockService {
String mockString();
}

@Service
public class MockServiceImpl implements MockService{

@Override
public String mockString() {return "business return";}
}

单元测试

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
//测试基类
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = SpringServiceTestApplication.class)
@ExtendWith(MockitoExtension.class)
public class BaseTest {}

//集成测试
@RunWith(JUnitPlatform.class)
@SelectPackages("org.spring.demo.demo")
public class TestAll extends BaseTest {}

//业务测试 1
class Test1 extends BaseTest {
@InjectMocks
@Resource
private MockOuterService mockOuterService;

@Spy
private MockService mockService;

@Test
void test(){
Mockito.when(mockService.mockString()).thenReturn("test1 mock");
System.out.println(mockOuterService.mockString());
}
}

//业务测试 2
class Test2 extends BaseTest {


@SpyBean
private MockService mockService;

@Resource
private MockOuterService mockOuterService;

@Test
void test(){
Mockito.when(mockService.mockString()).thenReturn("test2 mock");
System.out.println(mockOuterService.mockString());
}
}


运行截图

Spring Boot Banner 出现两次,容器被启动两次。

重复加载原因

常见原因

Spring 是一个依赖上下文的框架,若上下文缓存失效/配置变更则会重新刷新上下文,从而引发容器重启。

推测原因

业务项目启动过程中,Spring Boot Banner 出现一次,且所有资源(数据库连接池/动态线程池)只初始化一遍。

而在单测过程中启动多次,有可能是单测改变了 Spring 上下文,或者单测与业务代码结合有问题。

最小可行性分析

在对单测删减到最后两个类,仅使用 @MockBean 或者 @InjectMocks/@Mock 时,容器启动一次。同时使用,容器启动两次。

所以原因应该是 SpringBoot 封装的与 Mockito 原生的同时作用情况下,改变了 Spring 的上下文。

根本原因

@MockBean/@SpyBean 作用原理

整体流程分为注册与使用两部份。

注册方面

  1. Junit 通过 Extension 形式提供单元测试各执行流程的自定义逻辑扩展能力。
  2. Spring 自定义 Extension 子类 SpringExtension,并通过执行不同 TestExecutionListener 执行 TestContextManager 自定义内容。
  3. Spring 通过 ContextCustomizerFactory 形式提供动态修改上下文的能力,SpringBoot test 实现子类用于 Mock 相关逻辑实现。
  4. SpringBoot test自定义 ContextCustomizerFactory 子类 MockitoContextCustomizerFactory,用于 Mock 相关逻辑实现。

使用方面

Junit 通过执行不同 Extension 实现自定义逻辑,进而一层层触发到 SpringBoot test 实现的 Mock 相关逻辑。

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
@startuml
group 注册扩展
group junit5 内部流程
"MethodBasedTestDescriptor" -> "ExtensionUtils": 扫描 @Test,调用 Extension 注册
"ExtensionUtils" -> "MutableExtensionRegistry": 调用注册
"MutableExtensionRegistry" -> "SpringExtension": 注册 Extension
end
group Spring Framework 逻辑
"SpringExtension" -> "TestContextManager": 新建 TestContextManager
"TestContextManager" -> "BootstrapUtils": 触发 @BootstrapWith 扫描逻辑
"BootstrapUtils" -> "TestContextManager": 返回启动类
"TestContextManager" -> "SpringBootTestContextBootstrapper": 触发启动类 buildTestContext
end
group SpringBoot 逻辑
"SpringBootTestContextBootstrapper" -> "SpringBootTestContextBootstrapper": 扫描执行 ContextCustomizerFactory 逻辑
"SpringBootTestContextBootstrapper" -> "MockitoContextCustomizerFactory": Mockito 自定义上下文类
"MockitoContextCustomizerFactory" -> "DefinitionsParser": 注册注解扫描器
"DefinitionsParser" -> "DefinitionsParser": 扫描 @SpyBean/@MockBean 添加到 Definition 集合中
"DefinitionsParser" -> "MockitoContextCustomizerFactory": 返回 Definition 集合
"MockitoContextCustomizerFactory" -> "MockitoContextCustomizerFactory": 以 Definition 集合构建 ContextCustomizer
"MockitoContextCustomizerFactory" -> "SpringBootTestContextBootstrapper": 返回 ContextCustomizer
"SpringBootTestContextBootstrapper" -> "SpringBootTestContextBootstrapper": 构建 MergedContextConfiguration
"SpringBootTestContextBootstrapper" -> "SpringBootTestContextBootstrapper": 构建 TestContext
end
"SpringBootTestContextBootstrapper" -> "TestContextManager": 返回 TestContext
"TestContextManager" -> "TestContextManager": 存储上下文
"TestContextManager" -> "SpringExtension": 返回 TestContextManager 实例
"SpringExtension" -> "MutableExtensionRegistry": 返回 SpringExtension 实例
"MutableExtensionRegistry" -> "MutableExtensionRegistry": 存储各 Extension 实例
end
group 使用扩展
"NodeTestTask" -> "ClassBasedTestDescriptor": prepare
"ClassBasedTestDescriptor" -> "SpringExtension": TestInstancePostProcessor#postProcessTestInstance
"SpringExtension" -> "TestContextManager": prepareTestInstance
"TestContextManager" -> "DependencyInjectionTestExecutionListener": 传入 SpringBootTestContextBootstrapper 构建的 TestContext 到 prepareTestInstance
"DependencyInjectionTestExecutionListener" -> "DependencyInjectionTestExcutionListener": 创建完整 Spring Bean 依赖
end
@enduml

@InjectMocks +@Spy/@Mock 作用原理

整体流程分为注册与使用两部份。

注册方面,只是将自定义扩展挂载到 Junit 钩子上。

使用方面,扫描当前单测类相关注释,反射注入新增的 mock 对象。

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
@startuml
group 注册扩展
group junit5 内部流程
"MethodBasedTestDescriptor" -> "ExtensionUtils": 扫描 @Test,调用 Extension 注册
"ExtensionUtils" -> "MutableExtensionRegistry": 调用注册
"MutableExtensionRegistry" -> "MockitoExtension": 注册 Extension
end
end
group 使用扩展
group junit5 内部流程
"NodeTestTask" -> "TestMethodTestDescriptor": execute
"TestMethodTestDescriptor" -> "TestMethodTestDescriptor": TestMethodTestDescriptor#invokeBeforeEachCallbacks
"TestMethodTestDescriptor" -> "MockitoExtension": beforeEach
end
group mockito 内部流程
"MockitoExtension" -> "DefaultMockitoSessionBuilder": startMocking
"DefaultMockitoSessionBuilder" -> "DefaultMockitoSession": new DefaultMockitoSession Object
"DefaultMockitoSession" -> "MockitoAnnotations": openMocks
"MockitoAnnotations" -> "InjectingAnnotationEngine": process
"InjectingAnnotationEngine" -> "MockScanner": processIndependentAnnotations/injectCloseableMocks 传入当前单测类
"MockScanner" -> "MockScanner": 扫描修饰 @Mock/@Spy 变量
"MockScanner" -> "InjectingAnnotationEngine": 返回扫描结果
"InjectingAnnotationEngine" -> "InjectingAnnotationEngine": 执行 mock 注入
"InjectingAnnotationEngine" -> "DefaultInjectionEngine": injectMocksOnFields 反射注入
end
end
@enduml

二者原理区别

SpringBoot Mockito 原生
执行时机 单测类初始化(ClassBasedTestDescriptor) 单测方法(TestMethodTestDescriptor)
注入对象 缓存的 Spring Bean 调用 Java 原生反射构建

原因确定

根据二者的作用原理可知,Mockito 将 Spring Bean 依赖链改变,进而引发 Spring 容器的启动。

解决方案

根据原因可知,解决方案二选一:SpringBoot 的/Mockito 原生的。

SpringBoot Mockito 原生
针对对象 Spring Bean 所有的对象
使用复杂度 低,不需管理依赖链 高,需要手动注入被依赖的对象
作用场景 需 Spring 环境的集成测试 纯单元测试

解决

因为项目中使用 SpringBoot ,并且单元测试大多以集成测试的方式运行,结合使用的复杂度,所以选择了 SpringBoot 提供的方案。

仍需提升的

目前 junit-platform.properties 配置的策略为测试类串行/测试方法并行,希望改成测试类并行/测试方法并行,进一步提升速度。

但修改后发现大部份单测都报错了,仍需明确原因,推测是各单测自定义 mock 行为互相影响导致。

结尾

通过表象猜测原因,通过源码证明猜想。

在猜测原因时,就在想 SpringBoot 不可能只为了 KPI 而集成 Mockito,肯定有结合 Mockito 做对应的集成,方便后续使用。

追查源码后印证了猜想,满足。

本文首发于cartoon的博客

转载请注明出处:https://cartoonyu.github.io

背景

最近在看设计模式,在单例模式的 Double Check Lock(DCL)中,存在两个关键字:volatile & synchronized。

之前都知道 DCL 怎么写,直接套娃。但是这两关键字在单例里面的作用还没深究过,于是就自言自语一篇文章。

单例模式代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Single {

private static volatile Single INSTANCE;

private Single(){}

public static Single getInstance(){
//first check object
if(INSTANCE == null){
//locks in the class dimension
synchronized (Single.class){
//second check object
if(INSTANCE == null){
INSTANCE = new Single();
}
}
}
return INSTANCE;
}
}

synchronized & volatile 作用

synchronized 作用

synchronized 是 java 的重量级锁,可作用于方法维度、代码块维度。具体到底层字节码就是 monitor enter & monitor exit。

volatile 作用

volatile 有两个作用

  1. 保证可见性。

修饰的变量从主内存 copy 到工作内存,修改后写回到主内存。

作用原理就是读写前 JVM 加一个内存屏障,通过 happen before 原则确保修改可见于读取。

主内存:类比成 JVM 堆

工作内存:类比成 JVM 虚拟机栈

  1. 禁止指令重排序。

对象初始化步骤

  1. 分配内存空间
  2. 实例变量初始化
  3. 执行初始化代码块
  4. 调用构造函数
  5. 静态变量初始化
  6. 返回对象引用

刨除非必要步骤,上述 6 步剩下 3 步

  1. 分配内存空间
  2. 调用构造函数
  3. 返回对象引用

synchronized & volatile 在 DCL 缺失的问题

有 synchronized 没有 volatile

由于指令重排序的优化手段,上述创建对象的 1 -> 2 -> 3, 可能顺序变成了 1 -> 3 -> 2。

没有禁止重排序,顺序变了,中间的第三步返回是个空对象,并发情况下就 G 了,另一线程拿的是空对象。

有 volatile 没有 synchronized

并发情况下, A B 两个线程都通过了等于 null 的校验,开始执行创建对象的操作。

上述创建对象说的 1 -> 2 -> 3, A B 两个线程都有可能走完 1 & 2,最后单例对象会是执行慢的线程返回引用的那个。构造函数会执行两遍,违反了单例模式的初衷。

总结

synchronized & volatile 的作用就很明显。

synchronized 保障构造函数只执行一遍,对象也只有一个。

volatile 保障极端情况下不会返回空对象。

题外话

  1. 为什么 synchronized 修饰的是代码块,不是静态方法 getInstance ?

为了提升获取单例对象的运行效率。

synchronized 修饰代码块,只会影响进入代码块瞬间的其他线程。

synchronized 修饰方法,会影响调用方法的所有线程。

  1. 为什么进入 synchronized 修饰代码块后,还要判断一下单例对象是否等于 null ?

防止重复创建对象。

假设 A B 两个线程

  1. A 线程进入同步块,还在上述说的对象初始化三步里面,B 线程进来了,在 synchronized 代码块外自旋。
  2. A 线程执行完了,B 线程进代码块了,这时若不判断是否为 null,就会重复创建对象。

本文首发于cartoon的博客

转载请注明出处:https://cartoonyu.github.io

现象

在线上环境排查问题时,某个线程池在某个时间点新建线程达到设定的最大线程数 maximumPoolSize,后续流量降低后当前线程数仍未回落,仍然为最大线程数,阻塞队列中有任务,但是活跃线程数显著减少。

之前的认知

固有的认知中,线程池运行原理:java.util.concurrent.ThreadPoolExecutor#execute

  1. 线程池内部维护 corePoolSize 个线程
  2. 任务提交后,若核心线程都已被占用,则添加到阻塞队列
  3. 阻塞队列已满,则新建线程直到线程数到达 maximumPoolSize
  4. 若阻塞队列已满,并且线程数到达 maximumPoolSize,则执行拒绝策略
  5. 超过 corePoolSize 部分的空闲线程,到达 keepAliveTime 后,进行销毁。

冲突

认知第五点中:超过 corePoolSize 部分的空闲线程,到达 keepAliveTime 后,进行销毁。明显与现象不符。现象肯定没问题的,就是认知有问题了:超过 corePoolSize 部分的空闲线程,到达 keepAliveTime 后,至少不会马上销毁。

现实与认知的问题

  1. 超过 corePoolSize 部分的空闲线程,到达 keepAliveTime 后,会不会销毁?
  2. 销毁的时机是?
  3. 为什么线程池中大多为休眠线程?线程池的线程数仍为最大线程数?

重塑认知

答案都在源码内

ThreadPoolExecutor 执行任务流程

线程池使用 demo

1
2
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 10, 100, TimeUnit.MINUTES, new ArrayBlockingQueue<>(1000));
threadPoolExecutor.execute(() -> System.out.println("print in thread"));

执行流程

java.util.concurrent.ThreadPoolExecutor#execute

流程就是之前认知中 1 - 4 点,在第三点中蕴含一个重要变量:java.util.concurrent.ThreadPoolExecutor#workers,这个就是ThreadPoolExecutor 管理线程的对象

workers 移除流程

源码上看,只有以下两个方法

1
2
java.util.concurrent.ThreadPoolExecutor#addWorkerFailed
java.util.concurrent.ThreadPoolExecutor#processWorkerExit

望文生义,addWorkerFailed 作用为添加 worker 后的失败补偿动作,可以忽略这个方法。

所以正常的销毁动作,肯定是在 processWorkerExit 中。

processWorkerExit 执行流程

使用场景

仅在java.util.concurrent.ThreadPoolExecutor#runWorker 中 finally 执行

而 runWorker 则是任务执行的底层方法,那么这意味着:任务执行完,满足某几个前提条件就会销毁线程。那么前提条件是什么呢?

runWorker 执行流程

  1. while 循环调用 java.util.concurrent.ThreadPoolExecutor#getTask 获取任务
    1. 获取到任务后,走真实执行任务流程,beforeExecute/run/afterExecute
    2. 获取不到任务,则到 processWorkerExit 执行

getTask 执行流程

  1. 使用当前 worker 数与核心线程数关系判定变量 timed
  2. 根据 timed 判定 timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take()

keepAliveTime 第一次出现,并且是用于在当前 worker 数大于核心线程数情况下从阻塞队列中获取元素。

那么,控制 processWorkerExit 执行的前提条件:当前 worker 数大于核心线程数,并且从阻塞队列经过 keepAliveTime 拿不到任务。

但这个前提条件明显跟现象不符,那肯定是 workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) 中被阻塞了,导致实际获取任务时间 > keepAliveTime。

workQueue.poll 执行流程(以 ArrayBlockingQueue 为例)

  1. 获取 ArrayBlockingQueue 全局锁
  2. 当队列元素个数 = 0, 则 await keepAliveTime 时间
  3. 队列元素个数 != 0,出队元素
  4. 释放 ArrayBlockingQueue 全局锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0) {
if (nanos <= 0)
return null;
nanos = notEmpty.awaitNanos(nanos);
}
return dequeue();
} finally {
lock.unlock();
}
}

真相

从workQueue.poll 执行流程中,能明显看到线程 await 的前提是获取到队列的全局锁,并且队列元素 = 0。

整理一遍就是:

当线程获取到队列全局锁,并且当前队列为空,await keepAliveTime 后,若当前队列为空,则执行销毁方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@startuml

"Thread" -> "BlockingQueue": pool task
"Thread" -> "BlockingQueue": get global ReentrantLock
alt get global ReentrantLock success
alt BlockingQueue size = 0
"Thread" -> "Condition": await keepAliveTime
"BlockingQueue" -> "Thread": non task,execute processWorkerExit method
else
"BlockingQueue" -> "Thread": first task in queue
"Thread" -> "Thread": keep execute task
end
else
"Thread" -> "BlockingQueue": keep acquire ReentrantLock
end
@enduml

那么上述提到的两个问题

  1. 超过 corePoolSize 部分的空闲线程,到达 keepAliveTime 后,会不会销毁?
  2. 销毁的时机是?
  3. 为什么线程池中大多为休眠线程?线程池的线程数仍为最大线程数?

就有了答案

  1. 超过 corePoolSize 部分的空闲线程,到达 keepAliveTime 后,有可能销毁,前提是拿到队列的全局锁。

  2. 销毁的时机是当前线程获取到队列全局锁,并且队列元素 = 0,并且 await 后队列元素仍然为 0

  3. 因为线上提交任务刚好够核心线程消费,并且残留少数任务在阻塞队列中。在并发情况下,大部分线程都 await,线程池只能新增 worker 处理了。

自言自语

怎么解决当前线程数 = 最大线程数,并且活跃线程较少的情况?

  1. 调高 corePoolSize ,使线程池不新增 corePoolSize 之外的线程。
  2. 调低 keepAliveTime & TimeUnit 的值,使休眠线程快速被销毁。

在商业开发的角度上,比较难精准实现。

  1. 业务发展速度很快, corePoolSize 在将来的一段时间内就不适合了。
  2. 加快休眠线程的销毁,意味着存在频繁新建线程的问题,会影响系统稳定性。

为什么 await keepAliveTime后不直接销毁?还尝试出队元素?

这就回到 java 线程与操作系统线程的映射关系。

线程模型有三种:一对一,多对一,一对多。java 在大多数平台上都是一对一。

  1. 如果直接销毁,核心线程处理不过来情况下,线程池会频繁销毁/新建线程,消耗系统的资源。
  2. 尝试出队元素,double check 线程池的负载,负载高则继续处理,负载较低则销毁线程,达到节省资源的目的。

keepAliveTime 的理解

源码中的注释

1
2
3
when the number of threads is greater than the core, this is the maximum time that excess idle threads will wait for new tasks before terminating.

当线程数大于 Core 数时,这是多余的空闲线程在终止之前等待新任务的最长时间。

之前以为是线程数大于 Core 数时,空闲线程的存活时间。过了 keepAliveTime 就执行销毁。

现在认识到:线程数大于 Core 数时,空闲线程的存活时间 >= keepAliveTime (没获取到队列锁的情况下),并且销毁前 double check 是否有任务,没有才执行销毁。

本文首发于cartoon的博客

转载请注明出处:https://cartoonyu.github.io

提前叠个 buff:这个文章不涉及图(画起来比较麻烦),只是记录我的胡思乱想。

redis 从单点 -> 集群总共有三个部署模式:单机模式,主从模式,哨兵模式,集群模式

单机模式

新手入门模式。单机模式意味着 Redis 是单点的,部署在一台服务器,挂了就挂了,用在本地测试还可以,但是生产环境就算了。

优势

  1. 部署简单
  2. 省钱,一台服务器就可以了
  3. 不涉及主从复制等,数据强一致

劣势

  1. 单点意味着稳定性基本上为 0,挂了就挂了
  2. 吞吐量受限于单机资源

主从模式

当流量越来越大,单台机器资源不能无限增长,就需要水平扩展到多个节点,使用多个节点分散承接读流量。

主从模式为主节点承接写流量,从节点承接读流量,二者数据一致通过主节点异步复制(全量复制 / 增量复制)到从节点。

优势

  1. 读流量被分摊到多个节点上,读流量支持力度变大
  2. 当主节点宕机/不可用时,可以手动切换主节点继续提供服务

劣势

  1. 当主节点宕机/不可用时手动切换节点,切换过程中 redis (主节点)不可用,并且会丢失主节点 / 从节点之间未同步的数据
  2. 稳定性还是不够,依赖手动切换。不适用于生产。
  3. 写流量还是让主节点独自承受,写流量还是靠单机资源支撑

哨兵模式

哨兵模式主要解决主从模式中手动切换的部分,本质上哨兵代替了人,通过 gossip 协议监控主节点的健康情况。

优势

  1. 不用手动切换主节点了,切换过程中虽然 redis 也是不可用的,但是这个时间会极大的降低

劣势

  1. sentinel 与主节点多了一层心跳检测,有可能 sentinel 与主节点的网络抖动导致重新选举主节点。
  2. redis 主从节点吞吐因心跳检测可能稍微降低。

集群模式

集群模式主要解决了两个问题:写流量水平扩展 & 哨兵与主节点的网络抖动。

集群模式主要的架构为:主节点平分 16384 个槽,集群支持主节点的动态上线/下线(需要 rehash),主节点与从节点通过心跳关联,主节点失联后从节点有权发起选举成为主节点(raft 算法)。

优势

  1. 自管理集群内主从节点上下线,减少因外部集群网络抖动之类的发起的无效选举
  2. 数据按照 slot 存放在多个节点,客户端通过服务端主节点的重定向跳转到具体的槽,可动态调整数据分布
  3. 减少了集群整体不可用的概率,某一主节点宕机只影响一部分数据的访问
  4. 写流量 & 数据平分到多个节点,集群的写请求瓶颈得到缓解

劣势

  1. 集群间状态同步使用 gossip 协议,节点数较多存在较多的心跳网络流量
  2. 主节点的上线/下线需要进行 rehash ,当节点内数据较多耗时较长

redis 节点间复制有两种:全量复制 & 部分复制

全量复制

出现场景

  1. 从节点刚上线需要同步主节点的数据
  2. 从节点切换脑裂后从节点偏移量与主节点不一致的时间点
  3. 从节点偏移量不在主节点的复制缓冲区中

过程

  1. 从节点向主节点发起同步数据的请求
  2. 主节点通过 bgsave 形成当前数据的快照,发给从节点
  3. 从节点删除历史数据,加载主节点发过来 RDB 文件
  4. 从节点拉取主节点缓冲区数据,加载到自身的内存中,并更新当前的偏移量

部分复制

出现场景

  1. 全量复制出现场景之外的场景
  2. 主从日常复制

过程

  1. 主节点将命令同步到缓冲区(AOF)
  2. 从节点拉取缓冲区数据,更新到自身的节点中,并更新当前的偏移量

本文首发于cartoon的博客

转载请注明出处:https://cartoonyu.github.io

前言

缓存一致性常见的更新策略也比较多,如先更新数据库再更新缓存,先删缓存再更新数据库等等,我在理解的时候有些混乱,所以这个文章提供了一些理解上的技巧去理解缓存一致性。

为什么会有缓存一致性的问题

  1. 缓存与数据库是两套中间件,存在网络抖动之类的原因导致没有更新任一方的可能
  2. 数据库大多都是事务型的中间件,支持错误回滚,缓存大多是非事务型的中间件,这里缓存更新失败了没办法回滚

所以根因是缓存大部分不支持事务无法回滚。

怎么尽量解决缓存一致性的问题

操作二者必定有先后顺序,存在以下两个情况:

  1. 先操作缓存,再操作数据库。操作缓存成功,数据库更新失败,缓存无法回滚,数据不一致
  2. 先操作数据库,再操作缓存。操作数据库成功,缓存操作失败,可触发异常回滚数据库,数据一致

根据上述所列,只能先操作数据库,再操作缓存了。

操作缓存也分两种:

  1. 更新缓存数据,可能并发请求,后一请求更新缓存的数据被前一请求的更新覆盖了,导致数据不一致
  2. 删除缓存数据,并发请求,二者都使缓存失效,查询请求将数据库数据加载到缓存中,数据一致

根据上述所列,只能使缓存失效,查询请求加载数据到缓存中了。

所以,如果在不加任何重试措施的情况下,先操作数据库,再删除缓存是一个容错较好的方法。

缓存一致性的分类 & 存在的问题

Client 维护缓存 & 数据库的一致性

  1. 更新缓存 -> 更新数据库

    图片1

1
2
3
4
5
6
7
8
9
10
11
@startuml
Database Database as DB
entity Cache as Cache

transaction1 -> Cache: update data
transaction1 <-- Cache: update result

transaction1 -> DB: update data
transaction1 <-- DB: update result

@enduml
  • 可能出现的数据不一致

​ 数据不一致:更新缓存成功了,更新数据库失败了,有数据不一致的问题,直到缓存超时失效或又一更新请求操作成功都会不一致

  • 改进方式

    若保证更新数据仅有少数的服务更新,可以将更新数据库请求入队处理,且可加入重试机制。但是队列的加入会增大系统复杂度,并且重试以及缓存更新顺序不一致会加剧数据不一致

  1. 更新数据库 -> 更新缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@startuml
Database Database as DB
entity Cache as Cache

transaction1 -> DB: update data
transaction1 <-- DB: update result

transaction2 -> DB: update data
transaction2 <-- DB: update result


transaction2 -> Cache: update data
transaction2 <-- Cache: update result

transaction1 -> Cache: update data
transaction1 <-- Cache: update result

@enduml
  • 可能出现的数据不一致

​ 数据不一致:如 t1 先更新数据库,t2 在 t1 更新缓存前把数据库缓存都更新完了,t1 再更新缓存,这时候缓存上是 t1 的数据,数据库是 t2 的数据

  • 改进方式

    若保证更新数据仅有少数的服务更新,可以将更新数据库请求入队处理,但是队列更新的引入增大了系统复杂度

  1. 删除缓存 -> 更新数据库

1
2
3
4
5
6
7
8
9
10
11
12
@startuml
Database Database as DB
entity Cache as Cache

transaction1 -> Cache: delete data

query1 -> DB: select data
query1 -> Cache: insert data

transaction1 -> DB: update result

@enduml
  • 可能出现的数据不一致

    1. 如图所示,更新请求先删除缓存,查询请求从缓存获取不到数据从数据库获取数据(老数据)加载到缓存中,更新请求更新数据库
    2. 这样的流程会导致查询请求加载老数据到缓存中,后续更新请求更新新数据到数据库中,导致数据不一致
  • 改进方式

    暂无。

  1. 更新数据库 -> 删除缓存

1
2
3
4
5
6
7
8
9
10
@startuml
Database Database as DB
entity Cache as Cache

query1 -> DB: select data
transaction1 -> DB: update result
transaction1 -> Cache: delete data
query1 -> Cache: insert data

@enduml
  • 可能出现的数据不一致

    查询请求先拿到数据,在插入缓存前更新请求进来更新数据库并使缓存失效,这个请求比较罕见

    1. 发生的场景
      1. 查询请求所在机器请求缓存比更新请求做完的整个流程都要慢
    2. 发生的概率
      1. 很低。因为操作缓存一般会比操作数据库要快
  • 改进方式

    1. 变更数据记录变更事件
      1. 步骤
        1. 更新数据同步记录一个事件在本地内存中
        2. 查询请求在插入缓存前查询事件,如果存在变更则查数据库获取最新数据
        3. 如果此数据在查询请求插入缓存过程中一直变更,这里需要先返回当前数据库结果给上游,再开异步任务轮训事件/数据库插入缓存
      2. 适用场景
        1. 只适用单节点

Server 维护缓存 & 数据库的一致性

  1. Read though/Write though

    • read though

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @startuml
    Database Database as DB
    entity Cache as Cache

    query -> repository: select data

    repository -> cache: get data
    repository -> DB: get data
    DB -> repository: return data
    repository -> cache: update data
    repository -> query: return data

    @enduml
    • wirte though

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @startuml
    Database Database as DB
    entity Cache as Cache

    transcation -> repository: update data

    repository -> cache: update data
    repository -> DB: update data
    DB -> repository: return result
    repository -> transcation: return result

    @enduml
  • 可能出现的数据不一致
    • 程序没有优雅关闭,更新请求先更新了缓存,但还没更新数据库,数据丢失
    • 更新缓存成功,更新数据库失败导致的数据不一致
  • 适用场景
    • 更新数据库极低概率失败
    • 程序有优雅关闭功能
  • 改进方式
    • 暂无
  1. Write Behind

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@startuml
Database Database as DB
entity Cache as Cache

query -> repository: query data

repository -> cache: query data
repository -> DB: query data
DB -> repository: return data
repository -> cache: update data

repository -> query: return data

@enduml

1
2
3
4
5
6
7
8
9
10
11
@startuml
Database Database as DB
entity Cache as Cache

transcation -> repository: update data

repository -> cache: update data

repository -> DB: batch update data

@enduml
  • 可能出现的数据不一致
    • 程序没有优雅关闭,更新请求先更新了缓存,但还没更新数据库,数据丢失
    • 批量更新数据库失败导致的数据不一致
  • 适用场景
    • 更新数据库极低概率失败
    • 程序有优雅关闭功能
  • 改进方式
    • 暂无

参考

https://coolshell.cn/articles/17416.html

本文首发于cartoon的博客

转载请注明出处:https://cartoonyu.github.io

前段时间在看 kafka 相关内容,发现 kafka “所有的”读写流量都在主 partition 上,从 partition 只负责备份数据。

那么为什么 kafka 从 partition 不跟其他中间件一样承接读流量?

读写分离的初衷

读写分离的初衷我觉得是利用读流量 & 写流量不同的特性做针对性的优化,而这两种流量我觉得区别如下

读流量 写流量
业务特性 展示类的业务 操作类业务
流量占比
可接受数据延迟 较大 非常小
增长的可预见性 高峰/安全攻击可能会突发增长 总体平稳

使用 kafka 的业务特征

  1. 操作型业务,consumer 消费 producer 生产的消息,进行自身业务,这个消息就类似于 trigger
  2. 可支撑的流量较大,并且可支撑下游 consumer 较多,rebalance 需要一定的时间

kafka 架构

  1. 以 topic 为单位,一 topic 可拆分多个 partition,每个 partition 都可以有多个从 partition,不同 partition 分布在不同 broker 上
  2. 以 partition 为单位,形成 AR(Assigned Repllicas),ISR(In Sync Repllicas),OSR(Out Sync Repllicas),主 partition 接收到消息后按照 ack 策略同步到 ISR 中从 partition
    1. ack = 0,producer 发出消息后就不管了
    2. ack = 1,producer 发出消息写入主 partition 所在 broker 的磁盘就算成功
    3. ack = all,producer 发出消息写入主 partition 以及 ISR 上所有副 partition 的磁盘才算成功

kafka 没有主从读写分离的原因

  1. 不能主从读写分离的原因
    1. kafka 承接的大多是操作型业务,这部分读操作对数据延迟非常敏感。
    2. kafka 主从同步为半同步复制,并且有部分 partition 在 OSR 上,数据延迟较大
    3. kafka 主 partition 接收到消息后,可以根据 ack 策略落盘,如果不是 all 的话存在数据丢失的风险
  2. 不需要主从读写分离的原因
    1. kafka 本身就是多 partition 的架构,不同 parition 在不同的 broker 上,多主节点的结构本身分流了流量
    2. kafka 本身就有成熟的 rebalance 机制,partition 上线与下线都比较无感

本文首发于cartoon的博客

转载请注明出处:https://cartoonyu.github.io

NOTICE:本文仅记录本人对 JVM G1 的小小理解,没有详细记录每个点,若有误可指出

内存区域

G1 将堆分为各个 region,大小通过 G1HeapRegionSize 指定

region 分类
按 region 大小分
  1. 普通 region,存放大小小于普通 refion 容量的一半的对象
  2. humongous 区域,存放大对象
按功能来分
  1. 新老代
  2. 老年代(humongous 只能在老年代)

新对象进入

  1. 新对象根据大小进入普通 region /humongous
  2. 记忆集维护
    1. 本 region 维护一个记忆集,记忆集都是别的 region 对象指向本 region 的引用
    2. 写前屏障,处理 SATB,将修改前引用对象放入 SATB 队列
    3. 写后屏障,标记被修改的对象所在卡表为 dirty card

垃圾回收

回收依据

维护每个 region 中垃圾的价值(回收获得的空间大小以及所需时间的比值)大小,通过最大 GC 时间(-XX:MaxGCPauseMillis)优先处理价值大的 region

回收分类
  1. Young GC
  2. Mixed GC(老年代中的内存比例超过IHOP)
回收步骤
  1. 初始标记
    1. 标记 GC Root 直接关联的对象
    2. 生成原始快照
    3. 修改 TAMS 的值
    4. 需要 Stop the world
  2. 并发标记
    1. 从 GC Root 开始进行可达性分析
    2. 处理 SATB 记录的引用变动的对象
  3. 最终标记
    1. 处于并发阶段遗留的少量 SATB 记录
  4. 筛选回收
    1. 计算各个 region 回收价值
    2. 回收
      1. 将回收 region 中存活对象挪到空 region 中
      2. 清空原有 region
标记过程中问题处理
  1. 在并发标记中,有新对象生成
    1. 通过 TAMS 划分特定区域
    2. 新对象只能放在 TAMS 区域中,并且默认是黑色的
  2. 在并发标记进行可达性分析,引用变动的对象处理
    1. 使用 SATB 记录灰色到白色删除的引用
    2. 在最终标记以灰色的对象为根,重新扫描一次
停顿分析
  1. 初始标记是 STW 的,但是只标记 GC Root,所以停顿时间较短
  2. 并发标记因为是与应用线程并发进行的,所以即使需要进行可达性分析,但是也不会停顿
  3. 最终标记,因为并发标记漏的对象比较少,所以即使 STW,停顿也不长
  4. 筛选回收,因为 G1 实际是用的是复制算法,复制对象时间可能较长,所以耗时较多是在此阶段

概念解释

三色标记法
  1. 黑色意义为被访问过的对象,引用都扫描过,并且确认最后是存活的,GC Root 默认为黑色
  2. 灰色意义为被访问过的对象,有一个引用未被扫描过,未确定是否存活
  3. 白色意义为未被访问过的对象
对并发标记中对象引用变化处理
SATB
  1. 全称为 Snapshot At The Beginning
  2. Region 包含 5 个指针
    1. bottom
    2. previous TAMS
    3. next TAMS
    4. top
    5. end
  3. 作用流程
    1. 并发标记中,新创建的对象在 next TAMS -> top 之间,此区间默认为黑色,默认存活
    2. 灰色对象删除指向白色的引用,记录下来
    3. 以记录下来的灰色为根,重新扫描
Incremental Update
  1. 黑色插入新的指向白色的引用,记录下来
  2. 并发扫描结束后,以记录下来的黑色为根,重新扫描一遍

参考

https://blog.51cto.com/u_15072811/4679940

本文首发于cartoon的博客

转载请注明出处:https://cartoonyu.github.io

近段时间在学习缓存相关知识的时候,看到了缓存更新策略,于是就根据自己的理解,写下这篇文章

分类
  • Cache Aside
  • Read / Write Though
  • Write Behind
Cache Aside
  1. 步骤

    1. 读请求未命中缓存,取数据库数据,并回写缓存
    2. 写请求先更新数据库,再让缓存失效
  2. 优点

    1. 实现简单,调用者可控制数据持久化的细节
  3. 缺点

    1. 上层需要同时管理缓存与持久化,调用较复杂
    2. 写请求与读请求并发,读请求持续时间比写请求长,可能会覆盖旧数据到缓存中
  4. 使用场景

    1. 允许缓存数据不准确的场景
    2. 因为并发情况下,可能造成脏数据的情况,所以 QPS 较低场景也可以适用
  5. 代码示例

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
public class CacheAside<T, K> implements CacheUpdate<T, K>{
private Map<K, T> map;

@Override
public T getData(K key) {
//if cache has data, return
return map.get(key);
}

@Override
public boolean updateData(K key, T data) {
map.remove(key, data);
return true;
}

@Override
public boolean addData(K key, T data) {
return Objects.nonNull(map.put(key, data));
}

@Override
public boolean removeData(K key) {
map.remove(key);
return true;
}

public CacheAside() {
map = new HashMap<>();
}
}
  1. 调用示例
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
public class CacheAsideClient<T, K> implements CacheUpdateClient<T, K>{

public CacheUpdateFactory<T, K> factory = CacheUpdateFactory.getInstance();

private CacheUpdate<T, K> cacheUpdate;

private DatabaseOperation<T, K> databaseOperation;

@Override
public T getData(K key){
//get data from cache
T dataFromCache = cacheUpdate.getData(key);
//if cache haven't, get from database and put to cache
if(Objects.nonNull(dataFromCache)){
return dataFromCache;
}
T dataFromDatabase = databaseOperation.getData(key);
cacheUpdate.addData(key, dataFromDatabase);
return dataFromDatabase;
}

@Override
public boolean updateData(K key, T data){
//update data to database
boolean updateToDatabaseRes = databaseOperation.updateData(key, data);
if(updateToDatabaseRes){
//invalid cache data
return cacheUpdate.removeData(key);
}
return false;
}

@Override
public boolean addData(K key, T data){
//add data to database
return databaseOperation.addData(key, data);
}

@Override
public boolean removeData(K key){
//remove from database
boolean removeFromDatabaseRes = databaseOperation.removeData(key);
if(removeFromDatabaseRes){
//invalid cache data
return cacheUpdate.removeData(key);
}
return false;
}

public CacheAsideClient() {
cacheUpdate = factory.getObject(CacheUpdateEnum.CACHE_ASIDE);
databaseOperation = (DatabaseOperation<T, K>) new MockDatabaseOperation<T>();
}
}
Read / Write Though
  1. 步骤

    1. 读/写请求都只依赖缓存
    2. 缓存数据同步持久化
  2. 优点

    1. 上层对数据是否持久化/持久化实现无感
  3. 缺点

    1. 同步持久化性能较低,但能有效保证数据一致性
  4. 使用场景

    1. 性能要求不高的场景
  5. 代码示例

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
public class ReadOrWriteThough<T, K> implements CacheUpdate<T, K>{

private DatabaseOperation<T, K> databaseOperation;

private Map<K, T> map;

@Override
public T getData(K key) {
//if cache has data, return
if(map.containsKey(key)){
return map.get(key);
}
//get data from database and write to cache
T data = databaseOperation.getData(key);
map.put(key, data);
return data;
}

@Override
public boolean updateData(K key, T data) {
map.put(key, data);
return databaseOperation.updateData(key, data);
}

@Override
public boolean addData(K key, T data) {
map.put(key, data);
return databaseOperation.addData(key, data);
}

@Override
public boolean removeData(K key) {
map.remove(key);
return databaseOperation.removeData(key);
}

public ReadOrWriteThough() {
databaseOperation = (DatabaseOperation<T, K>) new MockDatabaseOperation<>();
map = new HashMap<>();
}
}
  1. 调用示例
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
public class ReadOrWriteThoughClient<T, K> implements CacheUpdateClient<T, K>{

private CacheUpdateFactory<T, K> factory = CacheUpdateFactory.getInstance();

private CacheUpdate<T, K> cacheUpdate;

@Override
public T getData(K key) {
return cacheUpdate.getData(key);
}

@Override
public boolean updateData(K key, T data) {
return cacheUpdate.updateData(key, data);
}

@Override
public boolean addData(K key, T data) {
return cacheUpdate.addData(key, data);
}

@Override
public boolean removeData(K key) {
return cacheUpdate.removeData(key);
}

public ReadOrWriteThoughClient() {
cacheUpdate = factory.getObject(CacheUpdateEnum.READ_WRITE_THOUGH);
}
}
Write Behind
  1. 步骤

    1. 读/写请求都只依赖缓存
    2. 缓存数据异步批量持久化
  2. 优点

    1. 上层对数据是否持久化/持久化实现无感
    2. 异步持久化,性能较 Read /Write Though 提高
  3. 缺点

    1. 异步持久化可能会导致数据丢失
  4. 使用场景

    1. 性能要求较高的场景
    2. 允许持久化数据丢失场景
  5. 代码示例

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
public class WriteBehind<T, K> implements CacheUpdate<T, K> {

private Map<K, T> map;

private DatabaseOperation<T, K> databaseOperation;

private ThreadPoolExecutor threadPoolExecutor;

@Override
public T getData(K key) {
if(map.containsKey(key)){
return map.get(key);
}
T data = databaseOperation.getData(key);
map.put(key, data);
return data;
}

@Override
public boolean updateData(K key, T data) {
map.put(key, data);
threadPoolExecutor.execute(() -> databaseOperation.updateData(key, data));
return true;
}

@Override
public boolean addData(K key, T data) {
map.put(key, data);
threadPoolExecutor.execute(() -> databaseOperation.addData(key, data));
return true;
}

@Override
public boolean removeData(K key) {
map.remove(key);
threadPoolExecutor.execute(() -> databaseOperation.removeData(key));
return true;
}

public WriteBehind() {
map = new HashMap<>();
databaseOperation = (DatabaseOperation<T, K>) new MockDatabaseOperation<>();
threadPoolExecutor = new ThreadPoolExecutor(5, 10, 1000, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1000), new ThreadPoolExecutor.CallerRunsPolicy());
}

}
  1. 调用示例
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
public class WriteBehindClient<T, K> implements CacheUpdateClient<T, K>{

private CacheUpdateFactory<T, K> cacheUpdateFactory = CacheUpdateFactory.getInstance();

private CacheUpdate<T, K> cacheUpdate;

@Override
public T getData(K key) {
return cacheUpdate.getData(key);
}

@Override
public boolean updateData(K key, T data) {
return cacheUpdate.updateData(key, data);
}

@Override
public boolean addData(K key, T data) {
return cacheUpdate.addData(key, data);
}

@Override
public boolean removeData(K key) {
return cacheUpdate.removeData(key);
}

public WriteBehindClient() {
cacheUpdate = cacheUpdateFactory.getObject(CacheUpdateEnum.WRITE_BEHIND);
}
}
总结
分类 优点 缺点 使用场景
Cache Aside 1. 实现简单,调用者可控制数据持久化的细节 1. 写请求与读请求并发,读请求持续时间比写请求长,可能会覆盖旧数据到缓存中
2. 上层需要同时管理缓存与持久化,调用较复杂
1. 允许缓存数据不准确的场景
2. 因为并发情况下,可能造成脏数据的情况,所以 QPS 较低场景也可以适用
Read / Write Though 1. 上层对数据是否持久化/持久化实现无感 1. 同步持久化性能较低,但能有效保证数据一致性 1. 性能要求不高的场景
Write Behind 1. 上层对数据是否持久化/持久化实现无感
2. 异步持久化,性能较 Read /Write Though 提高
1. 异步持久化可能会导致数据丢失 1. 性能要求较高的场景
2. 允许持久化数据丢失场景

本文首发于cartoon的博客

转载请注明出处:https://cartoonyu.github.io

近段时间在服务器搭建中间件,发现物理内存不足,因为对性能要求不高,所以就求助 swap 了。

下面是 ubuntu 的 swap 构建的步骤

  1. 新建 swap 文件(/root/swapfile)
1
2
3
4
dd if=/dev/zero of=/root/swapfile bs=1M count=8192

swap 文件位置:/root/swapfile
swap 文件大小:bs(文件块)* count(块数)
  1. 格式化 swap
1
mkswap /root/swapfile
  1. 启动 swap
1
swapon /root/swapfile
  1. 开机自启动(非必要)

    1. 打开 /etc/fstab
    2. 在文件末尾追加以下内容
    1
    /root/swapfile swap swap defaults 0 0
  2. 关闭 swap (可在不需要)

1
swapoff -a

以上就是 Linux 下新建 swap 缓存的步骤。

本文首发于cartoon的博客

转载请注明出处:https://cartoonyu.github.io