Nếu bạn cần phải xử lý tập kết quả cơ sở dữ liệu lớn bằng Java, thì bạn có thể lựa chọn JDBC
cho phép bạn khả năng kiểm soát ứng dụng ở tầng thấp. Mặt khác, nếu bạn đã lựa chọn sử dụng ORM
trong ứng dụng của mình, nhưng sau đó thay đổi để sử dụng JDBC
thì điều này lại khiến bạn mất khá nhiều tính năng thú vị như optimistic locking, caching,... Rất may là hầu hết các ORM framework, ví dụ như Hibernate, có vài lựa chọn có thể giúp bạn có thể xử lý tập kết quả cơ sở dữ liệu lớn.
Một ví dụ đơn giản như sau, giả định chúng ta có một bảng (được ánh xạ tới class DemoEntity
) với một 100,000 bản ghi. Mỗi bản ghi bao gồm một cột đơn (được ánh xạ tới thuộc tính property
trong lớp DemoEntity
) giữ một số kí tự ngẫu nhiên với dung lượng ~ 2KB. JVM được chạy với -Xmx250m
- giả sử chúng ta chỉ có tối đa 250MB cho bộ nhớ JVM trên hệ thống. Công việc của bạn là đọc toàn bộ bản ghi có trong bảng, thực hiện một vài xử lý và sau đó lưu lại kết quả. Ta giả định rằng kết quả từ việc xử lý tập dữ liệu lớn không bị thay đổi. Để bắt đầu ta sẽ thử xử lý đơn giản trước, thực hiện truy vấn lấy toàn bộ dữ liệu:
new TransactionTemplate(txManager).execute(new TransactionCallback<Void>() {
@Override
public Void doInTransaction(TransactionStatus status) {
Session session = sessionFactory.getCurrentSession();
List<DemoEntity> demoEntitities = (List<DemoEntity>) session.createQuery("from DemoEntity").list();
for(DemoEntity demoEntity : demoEntitities) {
// Process and write result
}
return null;
}
});
Sau vài giây, ta nhận được message sau:
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
Hình 1:
Rõ ràng điều này không giải quyết được vấn đề của chúng ta. Để khắc phục vấn đề này, chúng ta sẽ chuyển qua sử dụng scrollable
trong Hibernate để thực thi câu truy vấn trên, ánh xạ toàn bộ kết quả vào các đối tượng entity
và return
chúng. Khi sử dụng scrollable result set, các bản ghi được biến đổi sang các entity trong cùng một lần:
new TransactionTemplate(txManager).execute(new TransactionCallback<Void>() {
@Override
public Void doInTransaction(TransactionStatus status) {
Session session = sessionFactory.getCurrentSession();
ScrollableResults scrollableResults = session.createQuery("from DemoEntity").scroll(ScrollMode.FORWARD_ONLY);
int count = 0;
while (scrollableResults.next()) {
if (++count > 0 && count % 100 == 0) {
System.out.println("Fetched " + count + " entities");
}
DemoEntity demoEntity = (DemoEntity) scrollableResults.get()[0];
// Process and write result
}
return null;
}
});
Sau khi chạy đoạn code trên:
...
Fetched 49800 entities
Fetched 49900 entities
Fetched 50000 entities
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
Hình 2:
Mặc dù ta đang sử dụng một scrollable result set
, nhưng mỗi đối tượng trả về là một attached object
và trở thành một phần của persistence context
(aka session). Kết quả này thực sự rất giống với ví dụ đầu tiên khi mà chúng ta sử dụng session.createQuery("from DemoEntity").list()
. Tuy nhiên, với phương cách đầu tiên ta không phải điều khiển bằng tay, chúng ta sẽ có được kết quả khi Hibernate hoàn thành công việc của nó. Mặt khác, bằng cách sử dụng một scrollable result set
sẽ cho phép chúng ta khả năng kiểm soát quá trình xử lý và giải phóng bộ nhớ khi cần. Như ta có thể thấy thì nó sẽ không tự động giải phóng bộ nhớ, bạn phải cho Hibernate biết khi nào sẽ thự hiện việc này. Bạn có thể tham khảo vài cách thực hiện như sau:
- Thu hồi vùng nhớ của đối tượng trong
persistent context
sau khi xử lý xong - Xóa toàn bộ
session
Chúng ta sẽ lựa chọn cách đầu tiên, ở ví dụ trên tại dòng số 13 (// Process and write result) ta sẽ thêm đoạn code sau:
session.evict(demoEntity);
Lưu ý:
- Nếu bạn đã thực hiện bất kì thay đổi nào trên một hoặc nhiều đối tượng
entity
hãy chắc chắn rằng bạn đã thực hiện lệnhflush session
trước khi thực hiệnevict
hoặcclear
, nếu không thì câu truy vấn sẽ bị giữ lại vì nếu thực hiện sau thì Hibernate sẽ không gửi các thay đổi này đến cơ sở dữ liệu Evict
hoặcclear
sẽ không xóa các đối tượng entity khỏisecond level cache
. Nếu bạn cho phép và đang sử dụngsecond level cache
, bạn có thể phương thứcsessionFactory.getCache().evictXxx()
nếu muốn loại bỏ đối tượng khỏisecond level cache
- Sau thời điểm bạn sử dụng
evict
trên một entity, thì nó sẽ không liên kết vớisession
nữa. Bất kì thay đổi nào được thực hiện trên đối tượngentity
sẽ không được tự động phản ánh vào trong database. Nếu bạn đang sử dụng lazy loading, khi truy cập vào bất kì thuộc tính nào mà không được load trước khi thực hiệnevict
thì một ngoại lệorg.hibernate.LazyInitializationException
sẽ được ném ra. Vì vậy, về cơ bản hãy đảm bảo xử lý xong các đối tượngentity
(hoặc ít nhất khởi tạo những thứ cần thiết) trước khi bạn sử dụngevict
hoặcclear
Sau khi chạy lại chương trình, ta sẽ thấy nó được thực thi thành công:
...
Fetched 99800 entities
Fetched 99900 entities
Fetched 100000 entities
Hình 3:
Bạn cũng có thể thiết lập câu truy vấn là read-only
, điều này cho phép Hibernate thực hiện thêm một vài tối ưu.
ScrollableResults scrollableResults = session.createQuery("from DemoEntity").setReadOnly(true).scroll(ScrollMode.FORWARD_ONLY);
Thực hiện điều này chỉ mang lại sự khác biệt rất nhỏ trong việc sử dụng bộ nhớ.
Chúng ta đã có thể xử lý được 100,000 bản ghi, nhưng Hibernate có một tùy chọn khác để xử lý số lượng lớn: Stateless session
. Bạn có thể lấy được scrollable result set
từ một stateless session
tương tự với cách làm với một session
thông thường. Một stateless session
nằm ngay bên trên của JDBC
. Gần như, Hibernate sẽ chạy ở chế độ tắt toàn bộ các tính năng, nghĩa là sẽ không có persistent context
, không có 2nd level caching
, không có dirty detection
, không có lazy loading
, về cơ bản là sẽ không có gì. Javadoc có mô tả như sau:
/**
* A command-oriented API for performing bulk operations against a database.
* A stateless session does not implement a first-level cache nor interact with any
* second-level cache, nor does it implement transactional write-behind or automatic
* dirty checking, nor do operations cascade to associated instances. Collections are
* ignored by a stateless session. Operations performed via a stateless session bypass
* Hibernate's event model and interceptors. Stateless sessions are vulnerable to data
* aliasing effects, due to the lack of a first-level cache. For certain kinds of
* transactions, a stateless session may perform slightly faster than a stateful session.
*
* @author Gavin King
*/
Điều duy nhất sẽ được thực hiện đó là chuyển đổi các bản ghi thành các đối tượng java. Đây có thể là một lựa chọn hấp dẫn bởi vì nó giúp bạn thoát khỏi ý nghĩ là khi nào sẽ cần phải evict
/flush
.
new TransactionTemplate(txManager).execute(new TransactionCallback<Void>() {
@Override
public Void doInTransaction(TransactionStatus status) {
sessionFactory.getCurrentSession().doWork(new Work() {
@Override
public void execute(Connection connection) throws SQLException {
StatelessSession statelessSession = sessionFactory.openStatelessSession(connection);
try {
ScrollableResults scrollableResults = statelessSession.createQuery("from DemoEntity").scroll(ScrollMode.FORWARD_ONLY);
int count = 0;
while (scrollableResults.next()) {
if (++count > 0 && count % 100 == 0) {
System.out.println("Fetched " + count + " entities");
}
DemoEntity demoEntity = (DemoEntity) scrollableResults.get()[0];
// Process and write result
}
} finally {
statelessSession.close();
}
}
});
return null;
}
});
Hình 4:
Bên cạnh thực tế là sử dụng stateless session
sẽ rất tối ưu bộ nhớ, thì khi sử nó cũng có một vài hạn chế, bạn có thể nhận thấy là ta phải thực hiện mở và đóng stateless session
bằng tay một cách tường minh: sẽ không có bất kì phương thức nào giống như sessionFactory.getCurrentStatelessSession()
hoặc bất kì cách thức tích hợp với Spring để quản lý stateless session
(vào thời điểm hiện tại). Mặc định khi mở một stateless session
sẽ cấp phát mới một đối tượng java.sql.Connection
(nếu bạn sử dụng phương thức openStatelessSession()
), do đó nó sẽ gián tiếp tạo ra một transaction thứ hai. Bạn có thể tránh được việc này bằng cách sử dụng API của Hibernate như trong ví dụ trên, ta sẽ lấy được current connection
và truyền đối số qua phương thức openStatelessSession(Connection connection)
. Ta thực hiện đóng session trong khối finally
sẽ không có tác động đến kết nối vật lý vì nó được quản lý bởi Spring: chỉ có connection logic bị đóng và một connection logic mới được tạo ra khi mở một stateless session
.
Như đã nói ở trước đó, Hibernate sẽ chạy trong chế độ tất cả tính năng bị vô hiệu và các đối tượng entity được trả về ở trạng thái tách rời. Nghĩa là, mỗi entity
mà bạn chỉnh sửa thì bạn sẽ phải gọi phương thức statelessSession.update(entity)
trực tiếp. Đầu tiên, ta sẽ thử chỉnh sửa một đối tượng entity:
new TransactionTemplate(txManager).execute(new TransactionCallback<Void>() {
@Override
public Void doInTransaction(TransactionStatus status) {
sessionFactory.getCurrentSession().doWork(new Work() {
@Override
public void execute(Connection connection) throws SQLException {
StatelessSession statelessSession = sessionFactory.openStatelessSession(connection);
try {
DemoEntity demoEntity = (DemoEntity) statelessSession.createQuery("from DemoEntity where id = 1").uniqueResult();
demoEntity.setProperty("test");
statelessSession.update(demoEntity);
} finally {
statelessSession.close();
}
}
});
return null;
}
});
Ý tưởng là chúng ta sẽ thực hiện mở một stateless session
với connection
hiện tại, và như javadoc StatelessSession
chỉ ra rằng sẽ không thực hiện ghi dữ liệu sau khi chỉnh sửa đổi dữ liệu, mỗi câu lệnh được thực hiện bởi stateless session
sẽ được gửi trực tiếp đến database. Cuối cùng, khi transaction (được bắt đầu bởi TransactionTemplate
) được commit thì kết quả sau khi được chỉnh sửa sẽ hiển thị trong database.