秒杀场景下对象池化引发的思考

秒杀场景下,涉及到短时间大量请求,虽然最终落到数据库的请求很少,但这过程会涉及到许多临时对象的创建(如订单请求DTO、库存DTO),导致频繁youngGC。Young GC在Stop-The-World(STW)期间,JVM必须暂停所有线程,会导致请求延迟飙升,违背秒杀系统的低延迟要求。因此需要引入对象池化技术。


1. 简单的对象池

1.1 SimpleObjectPool

这是一个简单的对象池模型,使用BlockingQueue存放对象,在使用完成后将对象丢回池中。

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

/**
* 自定义对象池:管理可复用的对象
*/
public class SimpleObjectPool<T> {
// 使用阻塞队列管理对象
private final BlockingQueue<T> pool;

// 对象工厂
private final ObjectFactory<T> factory;

/**
* 构造函数
*
* @param initialSize 初始对象数量
* @param maxSize 最大对象数量
* @param factory 对象创建工厂
*/
public SimpleObjectPool(int initialSize, int maxSize, ObjectFactory<T> factory) {
this.factory = factory; // 初始化工厂
this.pool = new LinkedBlockingQueue<>(maxSize);
initializePool(initialSize, factory);
}

/*
初始化池中的对象
*/
private void initializePool(int initialSize, ObjectFactory<T> factory) {
for (int i = 0; i < initialSize; i++) {
pool.add(factory.create());
}
}

/**
* 从池中借出对象
*
* @return 对象实例
* @throws InterruptedException 如果线程被中断
*/
public T borrowObject() throws InterruptedException {
// 阻塞直到有可用对象
return pool.take();
}

/**
* 归还对象到池中
*
* @param object 要归还的对象
*/
public void returnObject(T object) {
if (object != null) {
// 非阻塞方式归还
pool.offer(object);
}
}

/*
对象创建工厂,这是一个函数式接口
*/
public interface ObjectFactory<T> {
T create();
}


/*
需要回收使用的类,可以是订单、库存
*/
static class ExpensiveObject {
private static int count = 0;
private final int id;

public ExpensiveObject() {
this.id = ++count;
System.out.println("创建对象: ExpensiveObject-" + id);
}

public void doSomething() {
System.out.println(Thread.currentThread().getName() + " 获取到对象: ExpensiveObject-" + id);
}

public String getName(){
return "ExpensiveObject-" + id;
}
}


public static void main(String[] args) {

// 1. 创建对象池(初始2个对象,最大5个)
// 由于ObjectFactory<T>是一个函数式接口,因此可以接收方法引用(或lambda表达式)作为参数,只要方法签名保持一致即可
SimpleObjectPool<ExpensiveObject> pool = new SimpleObjectPool<>(
2, 3, ExpensiveObject::new
);

// 2. 多线程测试
Runnable task = () -> {
try {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " 尝试获取对象");
ExpensiveObject obj = pool.borrowObject();
obj.doSomething();
// 模拟业务耗时
Thread.sleep(3000);
pool.returnObject(obj);
System.out.println(threadName + " 归还对象:" + obj.getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
};

// 3. 启动5个线程(测试池的复用)
for (int i = 0; i < 5; i++) {
new Thread(task).start();
}
}
}

1.2 执行结果

执行main方法,运行结果如下。可以看出,总共只创建了2个对象,后面线程要等待前面线程归还后才能获得对象。

20250816173742

1.3 引伸-函数式接口

SimpleObjectPool构造方法需要一个ObjectFactory对象

1
2
3
4
5
public SimpleObjectPool(int initialSize, int maxSize, ObjectFactory<T> factory) {
this.factory = factory; // 初始化工厂
this.pool = new LinkedBlockingQueue<>(maxSize);
initializePool(initialSize, factory);
}

但是传入的却是一个方法引用

1
2
3
SimpleObjectPool<ExpensiveObject> pool = new SimpleObjectPool<>(
2, 3, ExpensiveObject::new
);

ExpensiveObject::new表示 引用 ExpensiveObject 的无参构造方法,等价于

1
() -> new ExpensiveObject()

Java 允许用 Lambda 表达式方法引用 来替代函数式接口的实现。ExpensiveObject::new 正好匹配 ObjectFactory.create() 的签名(无参,返回 ExpensiveObject 对象),所以可以直接使用。

另外,函数式接口是指 仅包含一个抽象方法(SAM,Single Abstract Method)的接口,可以用 @FunctionalInterface 注解标记(非强制,但推荐用于编译时检查)

2. Apache Commons Pool2

Apache Commons Pool2 是一个对象池化框架,用于管理对象池,可以有效地重用对象,减少创建和销毁对象的开销。

2.1. 基本概念

  • PooledObject: 池中管理的对象
  • ObjectPool: 对象池接口
  • PooledObjectFactory: 对象工厂,负责创建、销毁和验证对象
  • GenericObjectPool: 最常用的对象池实现

2.2. 快速开始

2.2.1 添加依赖

1
2
3
4
5
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.11.1</version>
</dependency>

2.2.2 基本使用示例

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
// 1. 创建对象工厂
PooledObjectFactory<MyObject> factory = new BasePooledObjectFactory<MyObject>() {
@Override
public MyObject create() throws Exception {
// 创建新对象
return new MyObject();
}

@Override
public PooledObject<MyObject> wrap(MyObject obj) {
// 包装对象
return new DefaultPooledObject<>(obj);
}
};

// 2. 创建对象池
ObjectPool<MyObject> pool = new GenericObjectPool<>(factory);

// 3. 使用对象池
try {
MyObject obj = pool.borrowObject(); // 从池中借出对象
try {
// 使用对象
obj.doSomething();
} finally {
pool.returnObject(obj); // 归还对象到池中
}
} catch (Exception e) {
e.printStackTrace();
}

2.3. 配置 GenericObjectPool

可以通过 GenericObjectPoolConfig 进行详细配置:

1
2
3
4
5
6
7
8
9
10
GenericObjectPoolConfig<MyObject> config = new GenericObjectPoolConfig<>();
config.setMaxTotal(8); // 最大对象数
config.setMaxIdle(8); // 最大空闲对象数
config.setMinIdle(4); // 最小空闲对象数
config.setTestOnBorrow(true); // 借出时验证
config.setTestOnReturn(true); // 归还时验证
config.setTestWhileIdle(true); // 空闲时验证
config.setTimeBetweenEvictionRunsMillis(60000); // 空闲对象检查周期

ObjectPool<MyObject> pool = new GenericObjectPool<>(factory, config);

2.4. 高级特性

2.4.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
public class MyObjectFactory extends BasePooledObjectFactory<MyObject> {
@Override
public MyObject create() throws Exception {
return new MyObject();
}

@Override
public PooledObject<MyObject> wrap(MyObject obj) {
return new DefaultPooledObject<>(obj);
}

@Override
public boolean validateObject(PooledObject<MyObject> p) {
// 验证对象是否有效
return p.getObject().isValid();
}

@Override
public void destroyObject(PooledObject<MyObject> p) throws Exception {
// 销毁对象
p.getObject().close();
}

@Override
public void activateObject(PooledObject<MyObject> p) throws Exception {
// 激活对象(从池中借出前调用)
p.getObject().initialize();
}

@Override
public void passivateObject(PooledObject<MyObject> p) throws Exception {
// 钝化对象(归还到池中后调用)
p.getObject().reset();
}
}

2.4.2 使用软引用对象池

1
SoftReferenceObjectPool<MyObject> pool = new SoftReferenceObjectPool<>(factory);

2.5. 最佳实践

  1. 总是确保归还对象:使用 try-finally 块确保对象归还
  2. 合理配置池大小:根据系统资源和需求调整
  3. 实现适当的验证:避免使用无效对象
  4. 监控池状态:定期检查池的使用情况
  5. 考虑使用连接池:对于数据库连接等资源,考虑使用专门的连接池如 HikariCP

2.6. 常见问题

2.6.1 对象泄漏

如果忘记归还对象,会导致池中对象耗尽。解决方案:

1
2
3
4
5
6
MyObject obj = pool.borrowObject();
try {
// 使用对象
} finally {
pool.returnObject(obj);
}

2.6.2 性能调优

  • 调整 maxTotalmaxIdle 参数
  • 根据使用模式决定是否启用对象验证
  • 考虑使用 Lifo (后进先出) 或 Fifo (先进先出) 策略
文章作者: SongGT
文章链接: http://www.songguangtao.xyz/2024/12/12/35.秒杀场景下对象池化引发的思考/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 SongGuangtao's Blog
大哥大嫂[微信打赏]
过年好[支付宝打赏]