Skip to content

Commit 9088db7

Browse files
authored
Merge pull request #108 from NewLifeX/copilot/support-transaction-message
Add transaction message support to Producer (prepare + commit/rollback)
2 parents 2b985d7 + 1625d1b commit 9088db7

5 files changed

Lines changed: 235 additions & 1 deletion

File tree

NewLife.RocketMQ/Producer.cs

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Collections.Concurrent;
2+
using System.Globalization;
23
using NewLife.Log;
34
using NewLife.Reflection;
45
using NewLife.RocketMQ.Client;
@@ -12,6 +13,8 @@ namespace NewLife.RocketMQ;
1213
/// <summary>生产者</summary>
1314
public class Producer : MqBase
1415
{
16+
private const Int32 CommitLogOffsetHexLength = 16;
17+
1518
#region 属性
1619
/// <summary>负载均衡。发布消息时,分发到各个队列的负载均衡算法,默认使用带权重的轮询</summary>
1720
public ILoadBalance LoadBalance { get; set; }
@@ -229,6 +232,36 @@ public virtual SendResult Publish(Object body, String tags, String keys, Int32 t
229232

230233
return Publish(message, null, timeout);
231234
}
235+
236+
/// <summary>发布事务消息(半消息)</summary>
237+
/// <param name="message">消息体</param>
238+
/// <param name="queue">目标队列</param>
239+
/// <param name="timeout"></param>
240+
/// <returns></returns>
241+
public virtual SendResult PublishTransaction(Message message, MessageQueue queue = null, Int32 timeout = -1)
242+
{
243+
if (message is null) throw new ArgumentNullException(nameof(message));
244+
245+
message.Properties["TRAN_MSG"] = "true";
246+
message.Properties["PGROUP"] = Group;
247+
248+
return Publish(message, queue, timeout);
249+
}
250+
251+
/// <summary>发布事务消息(半消息)</summary>
252+
/// <param name="body"></param>
253+
/// <param name="tags"></param>
254+
/// <param name="keys"></param>
255+
/// <param name="timeout"></param>
256+
/// <returns></returns>
257+
public virtual SendResult PublishTransaction(Object body, String tags = null, String keys = null, Int32 timeout = -1)
258+
{
259+
var message = CreateMessage(body);
260+
message.Tags = tags;
261+
message.Keys = keys;
262+
263+
return PublishTransaction(message, null, timeout);
264+
}
232265
#endregion
233266

234267
#region 异步发布消息
@@ -319,6 +352,21 @@ public virtual async Task<SendResult> PublishAsync(Message message, MessageQueue
319352
return null;
320353
}
321354

355+
/// <summary>异步发布事务消息(半消息)</summary>
356+
/// <param name="message">消息体</param>
357+
/// <param name="queue">目标队列</param>
358+
/// <param name="cancellationToken">取消令牌</param>
359+
/// <returns></returns>
360+
public virtual Task<SendResult> PublishTransactionAsync(Message message, MessageQueue queue = null, CancellationToken cancellationToken = default)
361+
{
362+
if (message is null) throw new ArgumentNullException(nameof(message));
363+
364+
message.Properties["TRAN_MSG"] = "true";
365+
message.Properties["PGROUP"] = Group;
366+
367+
return PublishAsync(message, queue, cancellationToken);
368+
}
369+
322370
/// <summary>发布消息</summary>
323371
/// <param name="body"></param>
324372
/// <returns></returns>
@@ -554,6 +602,60 @@ public virtual void PublishDelay(Object body, DelayTimeLevels level, String tags
554602
}
555603
#endregion
556604

605+
#region 结束事务消息
606+
/// <summary>结束事务消息。提交或回滚事务</summary>
607+
/// <param name="result">发布事务消息返回结果</param>
608+
/// <param name="state">事务状态</param>
609+
/// <param name="fromTransactionCheck">是否来自事务回查</param>
610+
public virtual void EndTransaction(SendResult result, TransactionState state, Boolean fromTransactionCheck = false)
611+
{
612+
if (result is null) throw new ArgumentNullException(nameof(result));
613+
if (result.Queue == null) throw new ArgumentNullException(nameof(result), "缺少队列信息");
614+
if (result.Queue.BrokerName.IsNullOrEmpty()) throw new ArgumentNullException(nameof(result), "缺少BrokerName");
615+
616+
var header = new EndTransactionRequestHeader
617+
{
618+
ProducerGroup = Group,
619+
TranStateTableOffset = result.QueueOffset,
620+
CommitLogOffset = GetCommitLogOffset(result.OffsetMsgId),
621+
CommitOrRollback = (Int32)state,
622+
FromTransactionCheck = fromTransactionCheck,
623+
MsgId = result.MsgId,
624+
TransactionId = result.TransactionId,
625+
};
626+
627+
var bk = GetBroker(result.Queue.BrokerName);
628+
bk.Invoke(RequestCode.END_TRANSACTION, null, header.GetProperties());
629+
}
630+
631+
/// <summary>异步结束事务消息。提交或回滚事务</summary>
632+
/// <param name="result">发布事务消息返回结果</param>
633+
/// <param name="state">事务状态</param>
634+
/// <param name="fromTransactionCheck">是否来自事务回查</param>
635+
/// <param name="cancellationToken">取消令牌</param>
636+
/// <returns></returns>
637+
public virtual async Task EndTransactionAsync(SendResult result, TransactionState state, Boolean fromTransactionCheck = false, CancellationToken cancellationToken = default)
638+
{
639+
if (result is null) throw new ArgumentNullException(nameof(result));
640+
if (result.Queue == null) throw new ArgumentNullException(nameof(result), "缺少队列信息");
641+
if (result.Queue.BrokerName.IsNullOrEmpty()) throw new ArgumentNullException(nameof(result), "缺少BrokerName");
642+
643+
var header = new EndTransactionRequestHeader
644+
{
645+
ProducerGroup = Group,
646+
TranStateTableOffset = result.QueueOffset,
647+
CommitLogOffset = GetCommitLogOffset(result.OffsetMsgId),
648+
CommitOrRollback = (Int32)state,
649+
FromTransactionCheck = fromTransactionCheck,
650+
MsgId = result.MsgId,
651+
TransactionId = result.TransactionId,
652+
};
653+
654+
var bk = GetBroker(result.Queue.BrokerName);
655+
await bk.InvokeAsync(RequestCode.END_TRANSACTION, null, header.GetProperties(), false, cancellationToken).ConfigureAwait(false);
656+
}
657+
#endregion
658+
557659
#region 辅助
558660
/// <summary>
559661
/// 创建消息,设计于支持用户重载以改变消息序列化行为
@@ -594,8 +696,18 @@ private SendMessageRequestHeader CreateHeader(Message message)
594696
DefaultTopicQueueNums = DefaultTopicQueueNums
595697
};
596698

699+
if (message.Properties.TryGetValue("TRAN_MSG", out var str) && str.ToBoolean()) smrh.SysFlag = (Int32)TransactionState.Prepared;
700+
597701
return smrh;
598702
}
703+
704+
private static Int64 GetCommitLogOffset(String offsetMsgId)
705+
{
706+
if (offsetMsgId.IsNullOrEmpty() || offsetMsgId.Length < CommitLogOffsetHexLength) return 0;
707+
708+
// OffsetMsgId尾部16位是8字节(Int64)的CommitLogOffset十六进制表示
709+
return Int64.TryParse(offsetMsgId.Substring(offsetMsgId.Length - CommitLogOffsetHexLength), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var rs) ? rs : 0;
710+
}
599711
#endregion
600712

601713
#region 选择Broker队列
@@ -752,4 +864,4 @@ internal void HandleReplyMessage(MessageExt message)
752864
}
753865
}
754866
#endregion
755-
}
867+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using System.Reflection;
2+
using NewLife.Reflection;
3+
4+
namespace NewLife.RocketMQ.Protocol;
5+
6+
/// <summary>结束事务请求头</summary>
7+
public class EndTransactionRequestHeader
8+
{
9+
#region 属性
10+
/// <summary>生产组</summary>
11+
public String ProducerGroup { get; set; }
12+
13+
/// <summary>事务状态表偏移</summary>
14+
public Int64 TranStateTableOffset { get; set; }
15+
16+
/// <summary>提交日志偏移</summary>
17+
public Int64 CommitLogOffset { get; set; }
18+
19+
/// <summary>提交或回滚标记</summary>
20+
public Int32 CommitOrRollback { get; set; }
21+
22+
/// <summary>是否来自事务回查</summary>
23+
public Boolean FromTransactionCheck { get; set; }
24+
25+
/// <summary>消息编号</summary>
26+
public String MsgId { get; set; }
27+
28+
/// <summary>事务编号</summary>
29+
public String TransactionId { get; set; }
30+
#endregion
31+
32+
#region 方法
33+
/// <summary>获取属性字典</summary>
34+
/// <returns></returns>
35+
public IDictionary<String, Object> GetProperties()
36+
{
37+
var dic = new Dictionary<String, Object>();
38+
foreach (var pi in GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance))
39+
{
40+
if (pi.GetIndexParameters().Length > 0) continue;
41+
var name = pi.Name;
42+
if (!name.IsNullOrEmpty()) name = Char.ToLowerInvariant(name[0]) + name.Substring(1);
43+
44+
dic[name] = this.GetValue(pi);
45+
}
46+
47+
return dic;
48+
}
49+
#endregion
50+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace NewLife.RocketMQ.Protocol;
2+
3+
/// <summary>事务状态</summary>
4+
public enum TransactionState
5+
{
6+
/// <summary>预备事务(半消息)</summary>
7+
Prepared = 4,
8+
9+
/// <summary>提交事务</summary>
10+
Commit = 8,
11+
12+
/// <summary>回滚事务</summary>
13+
Rollback = 12,
14+
}

Readme.MD

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,21 @@ consumer.OnConsume = (q, messages) =>
6262
consumer.Start();
6363
```
6464

65+
## 事务消息
66+
67+
通过 `PublishTransaction` 发送半消息,本地事务执行完成后调用 `EndTransaction` 提交或回滚:
68+
69+
```csharp
70+
var producer = new Producer { Topic = "tx_topic", Group = "tx_group", NameServerAddress = "127.0.0.1:9876" };
71+
producer.Start();
72+
73+
var sendResult = producer.PublishTransaction("订单创建");
74+
75+
// 本地事务成功时提交,失败时回滚
76+
producer.EndTransaction(sendResult, TransactionState.Commit);
77+
// producer.EndTransaction(sendResult, TransactionState.Rollback);
78+
```
79+
6580
## 新生命项目矩阵
6681
各项目默认支持net9.0/netstandard2.1/netstandard2.0/net4.62/net4.5,旧版(2024.0801)支持net4.0/net2.0
6782

XUnitTestRocketMQ/CommandTests.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
using System;
22
using System.IO;
3+
using System.Reflection;
34
using NewLife;
45
using NewLife.Data;
6+
using NewLife.RocketMQ;
57
using NewLife.RocketMQ.Protocol;
68
using NewLife.Serialization;
79
using Xunit;
@@ -802,4 +804,45 @@ 22 76 65 72 73 69 6f 6e 22 3a 34 35 33 7d
802804
var pk = cmd.Payload;
803805
Assert.Null(pk);
804806
}
807+
808+
[Fact]
809+
public void CreateHeader_TransactionMessage_SetsPreparedFlag()
810+
{
811+
var producer = new Producer { Topic = "nx_test", Group = "nx_group" };
812+
var message = new Message();
813+
message.SetBody("hello");
814+
message.Properties["TRAN_MSG"] = "true";
815+
message.Properties["PGROUP"] = "nx_group";
816+
817+
var method = typeof(Producer).GetMethod("CreateHeader", BindingFlags.Instance | BindingFlags.NonPublic);
818+
Assert.NotNull(method);
819+
var header = (SendMessageRequestHeader)method.Invoke(producer, new Object[] { message });
820+
821+
Assert.Equal((Int32)TransactionState.Prepared, header.SysFlag);
822+
}
823+
824+
[Fact]
825+
public void EndTransactionRequestHeader_ToProperties_UsesCamelCase()
826+
{
827+
var header = new EndTransactionRequestHeader
828+
{
829+
ProducerGroup = "nx_group",
830+
TranStateTableOffset = 11,
831+
CommitLogOffset = 22,
832+
CommitOrRollback = (Int32)TransactionState.Commit,
833+
FromTransactionCheck = false,
834+
MsgId = "msg_1",
835+
TransactionId = "tx_1",
836+
};
837+
838+
var ext = header.GetProperties();
839+
840+
Assert.Equal("nx_group", ext["producerGroup"]);
841+
Assert.Equal("11", ext["tranStateTableOffset"]?.ToString());
842+
Assert.Equal("22", ext["commitLogOffset"]?.ToString());
843+
Assert.Equal("8", ext["commitOrRollback"]?.ToString());
844+
Assert.Equal("False", ext["fromTransactionCheck"]?.ToString());
845+
Assert.Equal("msg_1", ext["msgId"]);
846+
Assert.Equal("tx_1", ext["transactionId"]);
847+
}
805848
}

0 commit comments

Comments
 (0)