分散式事務之Spring事務與JMS事務(二)

蔣老溼發表於2018-08-27

分散式事務之Spring事務與JMS事務(二)

Spring事務

Spring事務機制主要包括宣告式事務和程式設計式事務,宣告式事務讓我們從複雜的事務處理中得到解脫,程式設計式事務在實際開發中得不到廣泛使用,僅供學習參考。

事務抽象

spring的事務管理提供了統一的API介面支援不同的資源,提供宣告式事務管企且方便與Spring框架整合。spring的事務管理器使用抽象的設計方式實現,以下為spring中事務管理器的邏輯實現程式碼 (精簡了一部分,突出核心邏輯)

## 事務狀態
public interface TransactionStatus extends SavepointManager{
    boolean isNewTransaction();
    boolean hasSavepoint();
    void setRollbackOnly();
    boolean isRollbackOnly();
    boolean isCompleted();
}
## 事務定義
public interface TransactionDefinition{
    int getPropagationBehavior();
    int getIsolationLevel();
    String getName();
    int getTimeout();
    boolean isReadOnly();
}
## 事務管理器
public interface PlatformTransactionManager{
    TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
    void commit(TransactionStatus status) throws TransactionException;
    void rollback(TransactionStatus status) throws TransactionException;
}
複製程式碼

事務的傳播行為

Spring在TransactionDefinition介面中規定了 7 種型別的事務傳播行為,它們規定了事務方法和事務方法發生巢狀呼叫時事務如何進行傳播:
事務傳播行為型別:

事務傳播行為型別 說明
PROPAGATION_REQUIRED 如果當前沒有事務,就新建一個事務,如果已經存在一個事務中,加入到這個事務中。這是最常見的選擇。
PROPAGATION_SUPPORTS 支援當前事務,如果當前沒有事務,就以非事務方式執行。
PROPAGATION_MANDATORY 使用當前的事務,如果當前沒有事務,就丟擲異常。
PROPAGATION_REQUIRES_NEW 新建事務,如果當前存在事務,把當前事務掛起。
PROPAGATION_NOT_SUPPORTED 以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。
PROPAGATION_NEVER 以非事務方式執行,如果當前存在事務,則丟擲異常。
PROPAGATION_NESTED 如果當前存在事務,則在巢狀事務內執行。如果當前沒有事務,則執行與PROPAGATION_REQUIRED類似的操作。

當使用PROPAGATION_NESTED時,底層的資料來源必須基於JDBC 3.0,並且實現者需要支援儲存點事務機制。

事務隔離級別

spring如果沒有指定事務隔離級別的話,則spring的事務隔離級別跟資料庫的隔離級別走,資料庫是什麼隔離級別,spring就是什麼隔離級別。
Spring事務的隔離級別:

  1. ISOLATION_DEFAULT: 這是一個PlatfromTransactionManager預設的隔離級別,使用資料庫預設的事務隔離級別. 另外四個與JDBC的隔離級別相對應
  2. ISOLATION_READ_UNCOMMITTED: 這是事務最低的隔離級別,它充許令外一個事務可以看到這個事務未提交的資料。 這種隔離級別會產生髒讀,不可重複讀和幻像讀。
  3. ISOLATION_READ_COMMITTED: 保證一個事務修改的資料提交後才能被另外一個事務讀取。另外一個事務不能讀取該事務未提交的資料
  4. ISOLATION_REPEATABLE_READ: 這種事務隔離級別可以防止髒讀,不可重複讀。但是可能出現幻像讀。 它除了保證一個事務不能讀取另一個事務未提交的資料外,還保證了避免下面的情況產生(不可重複讀)。
  5. ISOLATION_SERIALIZABLE: 這是花費最高代價但是最可靠的事務隔離級別。事務被處理為順序執行。 除了防止髒讀,不可重複讀外,還避免了幻像讀。

接下來看一下程式碼方式與標籤方式的事務實現:

## 程式碼方式實現
public OrderService{
    @Autowire 
    PlatformTransactionManager txManager;
    void buyTicket(BuyTicketDTO dto){
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        TransactionStatus status = txManager.getTransaction(def);
        try{
            ## 執行業務程式碼
            txManager.commit(status);
        }catch(Exception e){
            txManager.rollback(status);
        }
    }
}

## Transaction標籤實現方式
public OrderService{
    @Transactonal
    void buyTick(BuyTicketDTO dto){
        // get and begin transaction manager from context
        try{
            /**業務程式碼*/
            // commit transaction
        }catch(Exception e){
            // rollback transaction
        }
    }
}
複製程式碼

分散式事務之Spring事務與JMS事務(二)
策略介面PlatformTransactionManager事務管理器的常見實現有:
DataSourceTransactionManager、JpTransactionManager、JmsTransactionManager、JtaTransactionManager

JPA簡介及事務實現

JPA是Java的一個規範(Java永續性API)。 它用於在Java物件和關聯式資料庫之間儲存資料。 JPA充當物件導向的領域模型和關聯式資料庫系統之間的橋樑。 由於JPA只是一個規範,它本身不執行任何操作。 它需要一個實現。 因此,像Hibernate,TopLink和iBatis這樣的ORM工具實現了JPA資料永續性規範。
關於JPA事務例項的程式碼:
domian實體物件

@Entity(name = "customer")
public class Customer {
    ## id 自增長
    @Id
    @GeneratedValue
    private Long id;
    ## 唯一索引
    @Column(name = "user_name", unique = true)
    private String username;
    private String password;
    private String role;

    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    public String getRole() {
        return role;
    }
    public void setRole(String role) {
        this.role = role;
    }
}
複製程式碼

dao 介面

// 繼成JpaRepository中的方法,其中已經包含基本的CRUD
public interface CustomerRepository extends JpaRepository<Customer, Long> {
    Customer findOneByUsername(String username);
}
複製程式碼

service 業務操作,以下2種事務實現的效果是一樣的,意在告訴大家如何使用程式碼的方式實現與註解宣告事務相同的效果。

@Service
public class CustomerServiceTxInAnnotation {
    private static final Logger LOG = LoggerFactory.getLogger(CustomerServiceTxInAnnotation.class);
    
    @Autowired
    private CustomerRepository customerRepository;
    ## 使用註解的方式宣告事務存在
    @Transactional
    public Customer create(Customer customer) {
        LOG.info("CustomerService In Annotation create customer:{}", customer.getUsername());
        if (customer.getId() != null) {
            throw new RuntimeException("使用者已經存在");
        }
        customer.setUsername("Annotation:" + customer.getUsername());
        return customerRepository.save(customer);
    }
}
複製程式碼
@Service
public class CustomerServiceTxInCode {
    private static final Logger LOG = LoggerFactory.getLogger(CustomerServiceTxInCode.class);

    @Autowired
    private CustomerRepository customerRepository;
    @Autowired
    private PlatformTransactionManager transactionManager;

    public Customer create(Customer customer) {
        LOG.info("CustomerService In Code create customer:{}", customer.getUsername());
        if (customer.getId() != null) {
            throw new RuntimeException("使用者已經存在");
        }
        ## 使用程式碼的方式來宣告事務存在
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        def.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
        ## 當使用ISOLATION_SERIALIZABLE級別時,如果外部沒事務存在,則本身建立事務,,所以submitError方法丟擲異常可以回滾
        //def.setPropagationBehavior(TransactionDefinition.PROPAGATION_NOT_SUPPORTED);
        ## 當使用PROPAGATION_REQUIRED級別時,如果外部沒事務存在,則本身也不存在事務,,所以submitError方法丟擲異常依然可以儲存資料成功 
        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        def.setTimeout(15);
        TransactionStatus status = transactionManager.getTransaction(def);
        try {
            customer.setUsername("Code:" + customer.getUsername());
            customerRepository.save(customer);
            submitError();
            transactionManager.commit(status);
            return customer;
        } catch (Exception e) {
            transactionManager.rollback(status);
            throw e;
        }
    }
    private void submitError(){
        throw new RuntimeException("some Data error ")
    }
}
複製程式碼

controller 控制層

@RestController
@RequestMapping("/api/customer")
public class CustomerResource {
    private static final Logger LOG = LoggerFactory.getLogger(CustomerResource.class);

    @Autowired
    private CustomerServiceTxInAnnotation customerService;
    @Autowired
    private CustomerServiceTxInCode customerServiceInCode;
    @Autowired
    private CustomerRepository customerRepository;

    @PostMapping("/annotation")
    public Customer createInAnnotation(@RequestBody Customer customer) {
        LOG.info("CustomerResource create in annotation create customer:{}", customer.getUsername());
        return customerService.create(customer);
    }
    @PostMapping("/code")
    public Customer createInCode(@RequestBody Customer customer) {
        LOG.info("CustomerResource create in code create customer:{}", customer.getUsername());
        return customerServiceInCode.create(customer);
    }
    @GetMapping("")
    public List<Customer> getAll() {
        return customerRepository.findAll();
    }
}
複製程式碼

接下來看一下程式的執行結果及JPA事務的管理過程:

分散式事務之Spring事務與JMS事務(二)
在整個事務管理過程中使用的是Spring事務控制,並且由相關ORM框架實現JPA規範

JMS事務原理

Spring JMS Session

  • 通過Session進行事務管理操作
  • Session 是一個thread-bound(執行緒範圍內)
  • 事務上下文:一個執行緒一個Session

Spring JMS事務型別

  • Session管理的事務-原生事務
  • 外部管理的事務-JmsTransactionManager、JTA

Srping JMS事務機制過程

Session原生事務:

分散式事務之Spring事務與JMS事務(二)
JmsTransactionManager事務:

分散式事務之Spring事務與JMS事務(二)

##註解方式注入Spring Bean
@EnableJms
@Configuration
public class JmsConfig {
    private static final Logger LOG = LoggerFactory.getLogger(CustomerService.class);

    @Bean
    public JmsTemplate initJmsTemplate(ConnectionFactory connectionFactory) {
        LOG.debug("init jms template with converter.");
        JmsTemplate template = new JmsTemplate();
        ## JmsTemplate使用的connectionFactory跟JmsTransactionManager使用的必須是同一個,不能在這裡封裝成caching之類的。
        template.setConnectionFactory(connectionFactory); 
        return template;
    }

    ## 這個用於設定 @JmsListener使用的containerFactory
    @Bean
    public JmsListenerContainerFactory<?> msgFactory(ConnectionFactory connectionFactory,
                                                     DefaultJmsListenerContainerFactoryConfigurer configurer,
                                                     PlatformTransactionManager transactionManager) {
        DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
        factory.setTransactionManager(transactionManager);
        factory.setCacheLevelName("CACHE_CONNECTION");
        factory.setReceiveTimeout(10000L);
        configurer.configure(factory, connectionFactory);
        return factory;
    }

    @Bean
    public PlatformTransactionManager transactionManager(ConnectionFactory connectionFactory) {
        return new JmsTransactionManager(connectionFactory);
    }
}

複製程式碼
@Service
public class CustomerService {
    private static final Logger LOG = LoggerFactory.getLogger(CustomerService.class);

    @Autowired
    JmsTemplate jmsTemplate;
    @Autowired
    private PlatformTransactionManager transactionManager;

    @PostConstruct
    public void init() {
        jmsTemplate.setReceiveTimeout(3000);
    }
    ## 原生事務
    @JmsListener(destination = "customer:msg:new", containerFactory = "msgFactory")
    public void handle(String msg) {
        LOG.debug("Get JMS message to from customer:{}", msg);
        String reply = "Replied - " + msg;
        jmsTemplate.convertAndSend("customer:msg:reply", reply);
        if (msg.contains("error")) {
            simulateError();
        }
    }
    ## JmsTransactionManager事務
    @JmsListener(destination = "customer:msg2:new", containerFactory = "msgFactory")
    public void handle2(String msg) {
        LOG.debug("Get JMS message2 to from customer:{}", msg);
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        def.setTimeout(15);
        TransactionStatus status = transactionManager.getTransaction(def);
        try {
            String reply = "Replied-2 - " + msg;
            jmsTemplate.convertAndSend("customer:msg:reply", reply);
            if (!msg.contains("error")) {
                transactionManager.commit(status);
            } else {
                transactionManager.rollback(status);
            }
        } catch (Exception e) {
            transactionManager.rollback(status);
            throw e;
        }
    }

    private void simulateError() {
        throw new RuntimeException("some Data error.");
    }
}
複製程式碼

Spring 本地事務

緊密依賴於底層資源管理器(例如資料庫連線 ),事務處理侷限在當前事務資源內。此種事務處理方式不存在對應用伺服器的依賴,因而部署靈活卻無法支援多資料來源的分散式事務。

  • Spring容器管理事務的生命週期
  • 通過Spring事務介面呼叫
  • 業務程式碼與具體事務的實現無關

在資料庫連線中使用本地事務示例如下:

public void transferAccount() { 
       Connection conn = null; 
       Statement stmt = null; 
       try{ 
           conn = getDataSource().getConnection(); 
           ## 將自動提交設定為 false,
           ## 若設定為 true 則資料庫將會把每一次資料更新認定為一個事務並自動提交
           conn.setAutoCommit(false);
           stmt = conn.createStatement(); 
           ## 將 A 賬戶中的金額減少 500 
           stmt.execute("\
           update t_account set amount = amount - 500 where account_id = 'A'");
           ## 將 B 賬戶中的金額增加 500 
           stmt.execute("\
           update t_account set amount = amount + 500 where account_id = 'B'");
           ## 提交事務
           conn.commit();
           ## 事務提交:轉賬的兩步操作同時成功
       } catch(SQLException sqle){            
           try{ 
               ## 發生異常,回滾在本事務中的操做
              conn.rollback();
               ## 事務回滾:轉賬的兩步操作完全撤銷
               stmt.close(); 
               conn.close(); 
           }catch(Exception ignore){ } 
           sqle.printStackTrace(); 
       } 
   }
複製程式碼

本地事務機制過程圖:

分散式事務之Spring事務與JMS事務(二)

Spring 外部(全域性)事務

  • 外部事務管理器提供事務管理
  • 通過Spring事務介面,呼叫外部管理器
  • 使用JNDI等方式獲取外部事務管理器的例項
  • 外部事務管理器一般由應用伺服器提供、如JBoss等

JNDI(Java Naming and Directory Interface,Java命名和目錄介面)是SUN公司提供的一種標準的Java命名系統介面,JNDI提供統一的客戶端API,通過不同的訪問提供者介面JNDI服務供應介面(SPI)的實現,由管理者將JNDI API對映為特定的命名服務和目錄系統,使得Java應用程式可以和這些命名服務和目錄服務之間進行互動。

相關文章