Có thể tìm thấy một ví dụ tại đây: https://github.com/afedulov/routing-data- nguồn .
Spring cung cấp một biến thể của DataSource, được gọi là AbstractRoutingDatasource
. Nó có thể được sử dụng thay cho việc triển khai DataSource tiêu chuẩn và cho phép cơ chế xác định DataSource cụ thể nào sẽ sử dụng cho mỗi hoạt động trong thời gian chạy. Tất cả những gì bạn cần làm là mở rộng nó và cung cấp triển khai determineCurrentLookupKey
trừu tượng phương pháp. Đây là nơi triển khai logic tùy chỉnh của bạn để xác định Nguồn dữ liệu cụ thể. Đối tượng trả về đóng vai trò như một khóa tra cứu. Nó thường là String hoặc en Enum, được sử dụng như một định nghĩa trong cấu hình Spring (chi tiết sẽ theo sau).
package website.fedulov.routing.RoutingDataSource
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DbContextHolder.getDbType();
}
}
Bạn có thể tự hỏi đối tượng DbContextHolder đó là gì và làm thế nào nó biết được mã định danh DataSource nào cần trả về? Hãy nhớ rằng determineCurrentLookupKey
phương thức sẽ được gọi bất cứ khi nào Trans transactionManager yêu cầu kết nối. Điều quan trọng cần nhớ là mỗi giao dịch được "liên kết" với một luồng riêng biệt. Chính xác hơn, Trans transactionManager liên kết Kết nối với luồng hiện tại. Do đó, để gửi các giao dịch khác nhau đến các Nguồn dữ liệu mục tiêu khác nhau, chúng tôi phải đảm bảo rằng mọi luồng có thể xác định một cách đáng tin cậy Nguồn dữ liệu nào được sử dụng. Điều này làm cho việc sử dụng các biến ThreadLocal để liên kết Nguồn dữ liệu cụ thể với một Chủ đề và do đó với một Giao dịch là điều tự nhiên. Đây là cách nó được thực hiện:
public enum DbType {
MASTER,
REPLICA1,
}
public class DbContextHolder {
private static final ThreadLocal<DbType> contextHolder = new ThreadLocal<DbType>();
public static void setDbType(DbType dbType) {
if(dbType == null){
throw new NullPointerException();
}
contextHolder.set(dbType);
}
public static DbType getDbType() {
return (DbType) contextHolder.get();
}
public static void clearDbType() {
contextHolder.remove();
}
}
Như bạn thấy, bạn cũng có thể sử dụng một enum làm khóa và Spring sẽ xử lý nó một cách chính xác dựa trên tên. Các khóa và cấu hình DataSource được liên kết có thể trông giống như sau:
....
<bean id="dataSource" class="website.fedulov.routing.RoutingDataSource">
<property name="targetDataSources">
<map key-type="com.sabienzia.routing.DbType">
<entry key="MASTER" value-ref="dataSourceMaster"/>
<entry key="REPLICA1" value-ref="dataSourceReplica"/>
</map>
</property>
<property name="defaultTargetDataSource" ref="dataSourceMaster"/>
</bean>
<bean id="dataSourceMaster" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="${db.master.url}"/>
<property name="username" value="${db.username}"/>
<property name="password" value="${db.password}"/>
</bean>
<bean id="dataSourceReplica" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="${db.replica.url}"/>
<property name="username" value="${db.username}"/>
<property name="password" value="${db.password}"/>
</bean>
Tại thời điểm này, bạn có thể thấy mình đang làm điều gì đó như sau:
@Service
public class BookService {
private final BookRepository bookRepository;
private final Mapper mapper;
@Inject
public BookService(BookRepository bookRepository, Mapper mapper) {
this.bookRepository = bookRepository;
this.mapper = mapper;
}
@Transactional(readOnly = true)
public Page<BookDTO> getBooks(Pageable p) {
DbContextHolder.setDbType(DbType.REPLICA1); // <----- set ThreadLocal DataSource lookup key
// all connection from here will go to REPLICA1
Page<Book> booksPage = callActionRepo.findAll(p);
List<BookDTO> pContent = CollectionMapper.map(mapper, callActionsPage.getContent(), BookDTO.class);
DbContextHolder.clearDbType(); // <----- clear ThreadLocal setting
return new PageImpl<BookDTO>(pContent, p, callActionsPage.getTotalElements());
}
...//other methods
Giờ đây, chúng tôi có thể kiểm soát DataSource nào sẽ được sử dụng và chuyển tiếp các yêu cầu theo ý muốn. Có vẻ tốt!
... Hay không? Trước hết, những lời gọi phương thức tĩnh đó đến một DbContextHolder kỳ diệu thực sự xuất hiện. Họ trông giống như họ không thuộc về logic kinh doanh. Và họ không. Chúng không những không truyền đạt được mục đích mà còn có vẻ mỏng manh và dễ xảy ra lỗi (quên làm sạch dbType thì sao). Và điều gì sẽ xảy ra nếu một ngoại lệ được đưa ra giữa setDbType và cleanDbType? Chúng ta không thể bỏ qua nó. Chúng tôi cần phải hoàn toàn chắc chắn rằng chúng tôi đã đặt lại dbType, nếu không Luồng được trả về ThreadPool có thể ở trạng thái "bị hỏng", đang cố gắng ghi vào một bản sao trong lần gọi tiếp theo. Vì vậy, chúng tôi cần điều này:
@Transactional(readOnly = true)
public Page<BookDTO> getBooks(Pageable p) {
try{
DbContextHolder.setDbType(DbType.REPLICA1); // <----- set ThreadLocal DataSource lookup key
// all connection from here will go to REPLICA1
Page<Book> booksPage = callActionRepo.findAll(p);
List<BookDTO> pContent = CollectionMapper.map(mapper, callActionsPage.getContent(), BookDTO.class);
DbContextHolder.clearDbType(); // <----- clear ThreadLocal setting
} catch (Exception e){
throw new RuntimeException(e);
} finally {
DbContextHolder.clearDbType(); // <----- make sure ThreadLocal setting is cleared
}
return new PageImpl<BookDTO>(pContent, p, callActionsPage.getTotalElements());
}
Rất tiếc >_<
! Điều này chắc chắn không giống như một cái gì đó tôi muốn đưa vào mọi phương pháp chỉ đọc. Chúng ta có thể làm tốt hơn không? Tất nhiên rồi! Mô hình "làm điều gì đó ở đầu một phương pháp, sau đó làm điều gì đó ở cuối" sẽ gây tiếng vang. Các khía cạnh để giải cứu!
Thật không may, bài đăng này đã quá dài để đề cập đến chủ đề của các khía cạnh tùy chỉnh. Bạn có thể theo dõi chi tiết cách sử dụng các khía cạnh bằng cách sử dụng này liên kết .