Several articles on distributed transactions have been mentioned earlier( Reliable message final consistency Distributed transaction - TCC Distributed transactions - 2PC, 3PC ), but we haven't actually fought yet. In this article, we will demonstrate how to implement a distributed transaction based on reliable message in. NET environment. The distributed transaction flow based on reliable messages is still relatively clear, but it is difficult to implement it one by one with code. Through analysis, it can be found that the key point of this transaction is to insert the corresponding process in front of and behind the real business logic. Obviously, this process can be simplified through AOP technology. So there is AgileDT. AgileDT uses Natasha to dynamically generate proxy classes at startup to complete the operations with the message part for you. Users only need to care about the core business logic. https://github.com/kklldog/AgileDT Open source is not easy, we have a lot ✨✨✨
review
Previous article( Reliable message final consistency )We introduce distributed transactions based on reliable messages in detail. In order to better understand the code of AgileDT, we still need to briefly review it.
The overall process of the scheme can be divided into the following steps:
- The active party sends a "to be confirmed" message to the reliable messaging service before the real business starts
- After receiving the message to be confirmed, the reliable message service will persist the message to the database
- If the above operation is successful, the active party will start the real business. If it fails, it will directly give up the execution of the business
- If the business execution is successful, a confirmation message is sent to the reliable message service. If the execution fails, a cancel message is sent to the reliable message service.
- If the reliable message service receives a "confirmation" message, the status of the message record in the update database is "to be sent". If the received message is "Cancel", the status of the update message is "cancelled"
- If the database updated in the previous step is "to be sent", the message will be delivered to MQ, and the status of the message record in the database will be changed to "sent"
- After the message was successfully delivered to MQ in the previous step, MQ will push the message to the passive party.
- The passive party starts processing the service after receiving the message
- If the business processing is successful, the passive party will ACK the MQ and this message will be removed from the MQ
- If the business processing is successful, a "completed" message is sent to the reliable message service
- The reliable messaging service updates the database message record after receiving the completed message. The message record is not completed
No more nonsense. Let's demonstrate how to use AgileDT to quickly implement a distributed transaction based on reliable messages. Let's use the classic scenario of giving points to members after placing an order.
Using AgileDT
Dependent component
- mysql
- rabbitmq
At present, mysql database is supported, but the data access component uses freesql, so it is easy to support other databases in the future. At present, the reliable message service used by the framework is rabbitmq.
Running server
Create a new database and a new table in the service
// crate event_message table on mysql create table if not exists event_message ( event_id varchar(36) not null primary key, biz_msg varchar(4000) null, status enum('Prepare', 'Done', 'WaitSend', 'Sent', 'Finish', 'Cancel') not null, create_time datetime(3) null, event_name varchar(255) null );
Use docker compose to run the server
version: "3" # optional since v1.27.0 services: agile_dt: image: "kklldog/agile_dt" ports: - "5000:5000" environment: - db:provider=mysql - db:conn= Database=agile_dt;Data Source=192.168.0.115;User Id=root;Password=mdsd;port=3306 - mq:userName=admin - mq:password=123456 - mq:host=192.168.0.115 - mq:port=5672
Install client
AgileDT client libraries need to be installed on both the active and passive sides
Install-Package AgileDT.Client
Active party usage
- Add transaction message table in business database
// crate event_message table on mysql create table if not exists event_message ( event_id varchar(36) not null primary key, biz_msg varchar(4000) null, status enum('Prepare', 'Done', 'WaitSend', 'Sent', 'Finish', 'Cancel') not null, create_time datetime(3) null, event_name varchar(255) null );
- Modify profile
stay appsettings.json Add the following nodes to the file: "agiledt": { "server": "http://localhost:5000", "db": { "provider": "mysql", "conn": "Database=agile_order;Data Source=192.168.0.125;User Id=dev;Password=dev@123f;port=13306" //"conn": "Database=agile_order;Data Source=192.168.0.115;User Id=root;Password=mdsd;port=3306" }, "mq": { "host": "192.168.0.125", //"host": "192.168.0.115", "userName": "admin", "password": "123456", "port": 5672 } }
- Inject AgileDT client service
public void ConfigureServices(IServiceCollection services) { services.AddAgileDT(); ... }
- Implement IEventService method The class that handles the business logic of the active party needs to implement the IEventService interface and mark that method as the real business method. AgileDT will scan these types at startup, generate proxy classes using AOP technology, and insert corresponding logic before and after business methods to communicate with reliable message services. Here are some points to note:
- Implement IEventService interface
- Use the DtEventBizMethod annotation to mark the business entry method
- The business method must be a virtual method
- Use the DtEventName annotation to mark the method name of the transaction. If not, use the class name
Note: in the end, the business method must use transactions to synchronously modify the status field of the message table to the done state. This operation framework can't help you implement it Note: if the business method fails, please throw an Exception. If not, the framework will consider the execution successful
public interface IAddOrderService:IEventService { bool AddOrder(Order order); } [DtEventName("orderservice.order_added")] public class AddOrderService : IAddOrderService { private readonly ILogger<AddOrderService> _logger; public AddOrderService(ILogger<AddOrderService> logger) { _logger = logger; } public string EventId { get; set; } [DtEventBizMethod] public virtual bool AddOrder(Order order) { var ret = false; //3. Write Order and modify event status must be written in the same transaction FreeSQL.Instance.Ado.Transaction(() => { order.EventId = EventId;//Add an event field in the order table to make order follow event_ Associated with the message table var ret0 = FreeSQL.Instance.Insert(order).ExecuteAffrows(); var ret1 = FreeSQL.Instance.Update<OrderService.Data.entities.EventMessage>() .Set(x => x.Status, MessageStatus.Done) .Where(x => x.EventId == EventId) .ExecuteAffrows(); ret = ret0 > 0 && ret1 > 0; }); return ret; } /// <summary> ///Construct the message content required for subsequent business processing /// </summary> /// <returns></returns> public string GetBizMsg() { //Here, you can construct the content of the business message delivered to MQ, such as the delivery order number, so that the subsequent passive party can use it when processing business var order = FreeSQL.Instance.Select<Order>().Where(x => x.EventId == EventId).First(); return order?.Id; } }
After implementing the IAddOrderService interface, you can use IAddOrderService to inject the implementation class as usual. For example, it is injected into the constructor of the Controller. Because AgileDT will automatically register you when it starts.
Note: the life cycle of IAddOrderService and implementation class is Scoped.
Passive party usage
- Create a table in the business party database or add fields to the business table For the passive party, it is not necessary to create a table here. But at least there must be a place to store events_ ID information, the simplest is to add event directly to the main business table_ ID field.
- Modify profile
stay appsettings.json Add the following nodes to the file: "agiledt": { "server": "http://localhost:5000", "db": { "provider": "mysql", "conn": "Database=agile_order;Data Source=192.168.0.125;User Id=dev;Password=dev@123f;port=13306" //"conn": "Database=agile_order;Data Source=192.168.0.115;User Id=root;Password=mdsd;port=3306" }, "mq": { "host": "192.168.0.125", //"host": "192.168.0.115", "userName": "admin", "password": "123456", "port": 5672 } }
- Inject AgileDT service
public void ConfigureServices(IServiceCollection services) { services.AddAgileDT(); ... }
- Implement the IEventMessageHandler interface The passive party needs to receive messages delivered by MQ, and these processing classes need to implement the IEventMessageHandler interface. When AgileDT is started, it will scan these classes and then establish a binding relationship with MQ.
- The DtEventName annotation must be used here to mark the name of the event to be processed
- The recover method must be wait-and-see
public interface IOrderAddedMessageHandler: IEventMessageHandler { } [DtEventName("orderservice.order_added")] public class OrderAddedMessageHandler: IOrderAddedMessageHandler { static object _lock = new object(); public bool Receive(EventMessage message) { var bizMsg = message.BizMsg; var eventId = message.EventId; string orderId = bizMsg; lock (_lock) { var entity = FreeSQL.Instance.Select<PointHistory>().Where(x => x.EventId == eventId).First(); if (entity == null) { var ret = FreeSQL.Instance.Insert(new PointHistory { Id = Guid.NewGuid().ToString(), EventId = message.EventId, OrderId = orderId, Points = 20, CreateTime = DateTime.Now }).ExecuteAffrows(); return ret > 0; } else { return true; } } } }