基於可靠訊息方案的分散式事務(四):接入Lottor服務

aoho發表於2019-03-02

上一篇文章中,通過Lottor Sample介紹了快速體驗分散式事務Lottor。本文將會介紹如何將微服務中的生產方和消費方服務接入Lottor。

場景描述

  • 生產方:User服務
  • 消費方:Auth服務
  • 事務管理方:Lottor Server

Lottor-Samples中的場景為:客戶端呼叫User服務建立一個使用者,使用者服務的user表中增加了一條使用者記錄。除此之外,還會呼叫Auth服務建立該使用者對應的角色和許可權資訊。

基於可靠訊息方案的分散式事務(四):接入Lottor服務

我們通過上面的請求流程圖入手,介紹接入Lottor服務。當您啟動好docker-compose中的元件時,會建立好兩個服務對應的user和auth資料庫。其中User和Auth服務所需要的初始化資料已經準備好,放在各自的classpath下,服務在啟動時會自動初始化資料庫,所需要的預置資料(如角色、許可權資訊)也放在sql檔案中。

Lottor客戶端API

Lottor Client中提供了一個ExternalNettyService介面,用以傳送三類訊息到Lottor Server:

  • 預提交訊息
  • 確認提交訊息
  • 消費完成訊息
public interface ExternalNettyService {

    /**
     * pre-commit msgs
     *
     * @param preCommitMsgs
     */
    public Boolean preSend(List<TransactionMsg> preCommitMsgs);

    /**
     * confirm msgs
     *
     * @param success
     */
    public void postSend(Boolean success, Object message);

    /**
     * msgs after consuming
     *
     * @param msg
     * @param success
     */
    public void consumedSend(TransactionMsg msg, Boolean success);
}
複製程式碼

預傳送#preSend的入參為預提交的訊息列表,一個生產者可能有對應的多個消費者;確認提交#postSend的入參為生產方本地事務執行的狀態,如果失敗,第二個引數記錄異常資訊;#consumedSend為消費方消費成功的傳送的非同步訊息,第一個入參為其接收到的事務訊息,第二個為消費的狀態。

事務訊息TransactionMsg

public class TransactionMsg implements Serializable {
    /**
     * 用於訊息的追溯
     */
    private String groupId;

    /**
     * 事務訊息id
     */
    @NonNull
    private String subTaskId;

    /**
     * 源服務,即呼叫發起方
     */
    private String source;

    /**
     * 目標方服務
     */
    private String target;

    /**
     * 執行的方法,適配成列舉
     */
    private String method;

    /**
     * 引數,即要傳遞的內容,可以為null
     */
    private Object args;

    /**
     * 建立時間
     */
    private Long createTime = Timestamp.valueOf(DateUtils.getCurrentDateTime()).getTime();

    /**
     * 操作結果資訊
     */
    private String message;

    /**
     * 更新時間
     */
    private Long updateTime;

    /**
     * 是否消費,預設為否
     *
     * {@linkplain com.blueskykong.lottor.common.enums.ConsumedStatus}
     */
    private int consumed = ConsumedStatus.UNCONSUMED.getStatus();
	 
	 ...
}
複製程式碼

在構建事務訊息時,事務訊息id、源服務、目標服務、目標方法和目標方法的傳參args都是必不可少的。消費方消費完之後,將會設定consumed的狀態,出現異常將會設定異常message資訊。

生產方-User服務

建立使用者時,需要建立對應的角色。生產方接入分為三步:

  • 傳送預提交訊息
  • 執行本地事務
  • 傳送確認提交的訊息

引入依賴

首先,需要引入Lottor客戶端的依賴:

    <dependency>
        <groupId>com.blueskykong</groupId>
        <artifactId>lottor-starter</artifactId>
        <version>2.0.0-SNAPSHOT</version>
    </dependency>
複製程式碼

發起呼叫

UserService中定義了建立使用者的方法,我們需要在執行本地事務之前,構造事務訊息並預傳送到Lottor Server(對應流程圖中的步驟1)。如果遇到預傳送失敗,則直接停止本地事務的執行。如果本地事務執行成功(對應步驟3),則傳送confirm訊息,否則傳送回滾訊息到Lottor Server(對應步驟4)。

@Service
public class UserServiceImpl implements UserService {

    private static final Logger LOGGER = LoggerFactory.getLogger(UserServiceImpl.class);

	 //注入ExternalNettyService
    @Autowired
    private ExternalNettyService nettyService;

    @Autowired
    private UserMapper userMapper;

    @Override
    @Transactional
    public Boolean createUser(UserEntity userEntity, StateEnum flag) {
        UserRoleDTO userRoleDTO = new UserRoleDTO(RoleEnum.ADMIN, userEntity.getId());
		 //構造消費方的TransactionMsg
        TransactionMsg transactionMsg = new TransactionMsg.Builder()
                .setSource(ServiceNameEnum.TEST_USER.getServiceName())
                .setTarget(ServiceNameEnum.TEST_AUTH.getServiceName())
                .setMethod(MethodNameEnum.AUTH_ROLE.getMethod())
                .setSubTaskId(IdWorkerUtils.getInstance().createUUID())
                .setArgs(userRoleDTO)
                .build();

        if (flag == StateEnum.CONSUME_FAIL) {
            userRoleDTO.setUserId(null);
            transactionMsg.setArgs(userRoleDTO);
        }

        //傳送預處理訊息
        if (!nettyService.preSend(Collections.singletonList(transactionMsg))) {
            return false;//預傳送失敗,本地事務停止執行
        }

        //local transaction本地事務
        try {
            LOGGER.debug("執行本地事務!");
            if (flag != StateEnum.PRODUCE_FAIL) {
                userMapper.saveUser(userEntity);
            } else {
                userMapper.saveUserFailure(userEntity);
            }
        } catch (Exception e) {
        	  //本地事務異常,傳送回滾訊息
            nettyService.postSend(false, e.getMessage());
            LOGGER.error("執行本地事務失敗,cause is 【{}】", e.getLocalizedMessage());
            return false;
        }
        //傳送確認訊息
        nettyService.postSend(true, null);
        return true;
    }

}
複製程式碼

程式碼如上所示,實現不是很複雜。本地事務執行前,必然已經成功傳送了預提交訊息,當本地事務執行成功,Lottor Client將會記錄本地事務執行的狀態,避免非同步傳送的確認訊息的丟失,便於後續的Lottor Server回查。

配置檔案

lottor:
  enabled: true
  core:
    cache: true  
    cache-type: redis
    tx-redis-config:
      host-name: localhost
      port: 6379
    serializer: kryo
    netty-serializer: kryo
    tx-manager-id: lottor

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/user?autoReconnect=true&useSSL=false
    continue-on-error: false
    initialize: true
    max-active: 50
    max-idle: 10
    max-wait: 10000
    min-evictable-idle-time-millis: 60000
    min-idle: 8
    name: dbcp1
    test-on-borrow: false
    test-on-return: false
    test-while-idle: false
    time-between-eviction-runs-millis: 5000
    username: root
    password: _123456_
    schema[0]: classpath:/user.sql
複製程式碼

如上為User服務的部分配置檔案,lottor.enabled: true開啟Lottor 客戶端服務。cache 開啟本地快取記錄。cache-type指定了本地事務記錄的快取方式,可以為redis或者MongoDB。serializer為序列化和反序列化方式。tx-manager-id為對應的Lottor Server的服務名。

Lottor Server

多個微服務的接入,對Lottor Server其實沒什麼侵入性。這裡需要注意的是,TransactionMsg中設定的sourcetarget欄位來源於lottor-common中的com.blueskykong.lottor.common.enums.ServiceNameEnum

public enum ServiceNameEnum {
    TEST_USER("user", "tx-user"),
    TEST_AUTH("auth", "tx-auth");
	//服務名
    String serviceName;
	//訊息中介軟體的topic
    String topic;
    
    ...
}
複製程式碼

訊息中介軟體的topic是在服務名的基礎上,加上tx-字首。消費方在設定訂閱的topic時,需要按照這樣的規則命名。Lottor Server完成的步驟為上面流程圖中的2(成功收到預提交訊息)和5(傳送事務訊息到指定的消費方),除此之外,還會定時輪詢異常狀態的事務組和事務訊息。

消費方-Auth服務

引入依賴

    <dependency>
        <groupId>com.blueskykong</groupId>
        <artifactId>lottor-starter</artifactId>
        <version>2.0.0-SNAPSHOT</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-stream</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
    </dependency>
複製程式碼

引入了Lottor客戶端starter,spring-cloud-stream用於消費方接收來自Lottor Server的事務訊息。

topic監聽

@Component
@EnableBinding({TestSink.class})
public class ListenerStream extends InitStreamHandler {
    private static final Logger LOGGER = LoggerFactory.getLogger(ListenerStream.class);

    @Autowired
    private RoleUserService roleUserService;

    @Autowired
    public ListenerStream(ExternalNettyService nettyService, ObjectSerializer objectSerializer) {
        super(nettyService, objectSerializer);
    }

    @StreamListener(TestSink.INPUT)
    public void processSMS(Message message) {
        //解析接收到的TransactionMsg
        process(init(message));
    }

    @Transactional
    public void process(TransactionMsg message) {
        try {
            if (Objects.nonNull(message)) {
                LOGGER.info("===============consume notification message: =======================" + message.toString());
                if (StringUtils.isNotBlank(message.getMethod())) {
                    MethodNameEnum method = MethodNameEnum.fromString(message.getMethod());
                    LOGGER.info(message.getMethod());
                    //根據目標方法進行處理,因為一個服務可以對應多個生產方,有多個目標方法
                    switch (method) {
                        case AUTH_ROLE:
                            UserRoleDTO userRoleDTO = (UserRoleDTO) message.getArgs();
                            RoleEntity roleEntity = roleUserService.getRole(userRoleDTO.getRoleEnum().getName());
                            String roleId = "";
                            if (Objects.nonNull(roleEntity)) {
                                roleId = roleEntity.getId();
                            }
                            roleUserService.saveRoleUser(new UserRole(UUID.randomUUID().toString(), userRoleDTO.getUserId(), roleId));
                            LOGGER.info("matched case {}", MethodNameEnum.AUTH_ROLE);

                            break;
                        default:
                            LOGGER.warn("no matched consumer case!");
                            message.setMessage("no matched consumer case!");
                            nettyService.consumedSend(message, false);
                            return;
                    }
                }
            }
        } catch (Exception e) {
        	  //處理異常,傳送消費失敗的訊息
            LOGGER.error(e.getLocalizedMessage());
            message.setMessage(e.getLocalizedMessage());
            nettyService.consumedSend(message, false);
            return;
        }
        //成功消費
        nettyService.consumedSend(message, true);
        return;
    }
}
複製程式碼

消費方監聽指定的topic(如上實現中,為test-input中指定的topic,spring-cloud-stream更加簡便呼叫的介面),解析接收到的TransactionMsg。根據目標方法進行處理,因為一個服務可以對應多個生產方,有多個目標方法。執行本地事務時,Auth會根據TransactionMsg中提供的args作為入參執行指定的方法(對應步驟7),最後向Lottor Server傳送消費的結果(對應步驟8)。

配置檔案

---
spring:
  cloud:
    stream:
      bindings:
        test-input:
          group: testGroup
          content-type: application/x-java-object;type=com.blueskykong.lottor.common.entity.TransactionMsgAdapter
          destination: tx-auth
          binder: rabbit1
      binders:
        rabbit1:
          type: rabbit
          environment:
            spring:
              rabbitmq:
                host: localhost
                port: 5672
                username: guest
                password: guest
                virtual-host: /

---
lottor:
  enabled: true
  core:
    cache: true
    cache-type: redis
    tx-redis-config:
      host-name: localhost
      port: 6379
    serializer: kryo
    netty-serializer: kryo
    tx-manager-id: lottor
複製程式碼

配置和User服務的差別在於增加了spring-cloud-stream的配置,配置了rabbitmq的相關資訊,監聽的topic為tx-auth。

小結

本文主要通過User和Auth的示例服務講解了如何接入Lottor客戶端。生產方構造涉及的事務訊息,首先預傳送事務訊息到Lottor Server,成功預提交之後便執行本地事務;本地事務執行完則非同步傳送確認訊息(可能成功,也可能失敗)。Lottor Server根據接收到的確認訊息決定是否將對應的事務組訊息傳送到對應的消費方。Lottor Server還會定時輪詢異常狀態的事務組和事務訊息,以防因為非同步的確認訊息傳送失敗。消費方收到事務訊息之後,將會根據目標方法執行對應的處理操作,最後將消費結果非同步回寫到Lottor Server。

推薦閱讀

基於可靠訊息方案的分散式事務

Lottor專案地址:https://github.com/keets2012/Lottor

訂閱最新文章,歡迎關注我的公眾號

基於可靠訊息方案的分散式事務(四):接入Lottor服務

相關文章