├── .gitignore
├── README.md
├── pom.xml
└── src
└── main
├── java
└── io
│ └── github
│ └── bty834
│ └── springtxmessage
│ ├── TxMessageCompensateSender.java
│ ├── TxMessageSendAdapter.java
│ ├── TxMessageSender.java
│ ├── config
│ ├── Note.java
│ └── TxMessageConfiguration.java
│ ├── model
│ ├── SendStatus.java
│ ├── TxMessage.java
│ ├── TxMessagePO.java
│ └── TxMessageSendResult.java
│ ├── support
│ ├── DefaultTxMessageCompensateSender.java
│ ├── DefaultTxMessageSender.java
│ └── TxMessageRepository.java
│ └── utils
│ ├── SnowFlake.java
│ └── TransactionUtil.java
└── resources
├── META-INF
└── spring.factories
└── local_message.sql
/.gitignore:
--------------------------------------------------------------------------------
1 | HELP.md
2 | target/
3 | !.mvn/wrapper/maven-wrapper.jar
4 | !**/src/main/**/target/
5 | !**/src/test/**/target/
6 |
7 | ### STS ###
8 | .apt_generated
9 | .classpath
10 | .factorypath
11 | .project
12 | .settings
13 | .springBeans
14 | .sts4-cache
15 |
16 | ### IntelliJ IDEA ###
17 | .idea
18 | *.iws
19 | *.iml
20 | *.ipr
21 |
22 | ### NetBeans ###
23 | /nbproject/private/
24 | /nbbuild/
25 | /dist/
26 | /nbdist/
27 | /.nb-gradle/
28 | build/
29 | !**/src/main/**/build/
30 | !**/src/test/**/build/
31 |
32 | ### VS Code ###
33 | .vscode/
34 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # Quick Start
3 |
4 | 1. add dependency
5 |
6 | ```xml
7 |
8 | io.github.bty834
9 | spring-tx-message
10 | 0.0.1-SNAPSHOT
11 |
12 | ```
13 |
14 | 2. create table and configure adapter and repository
15 |
16 | ```sql
17 | CREATE TABLE `your_table_name`
18 | (
19 | `id` bigint NOT NULL AUTO_INCREMENT,
20 | `number` bigint NOT NULL ,
21 | `topic` varchar(255) NOT NULL,
22 | `sharding_key` varchar(255) DEFAULT NULL,
23 | `msg_id` varchar(255) DEFAULT NULL,
24 | `send_status` tinyint NOT NULL DEFAULT '0',
25 | `content` longtext NOT NULL,
26 | `retry_times` tinyint NOT NULL DEFAULT '0',
27 | `next_retry_time` datetime NOT NULL,
28 | `deleted` tinyint NOT NULL DEFAULT '0',
29 | `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
30 | `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
31 | PRIMARY KEY (`id`),
32 | KEY `number` (`number`),
33 | KEY `idx_createtime` (`create_time`),
34 | KEY `idx_msgid` (`msg_id`),
35 | KEY `idx_updatetime` (`update_time`),
36 | KEY `idx_nextretrytime_retrytimes_sendstatus_deleted` (`send_status`,`next_retry_time`, `retry_times`, `deleted`)
37 | ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4
38 | ```
39 |
40 | ```java
41 | import io.github.bty834.springtxmessage.utils.SnowFlake;
42 | import org.springframework.context.annotation.Bean;
43 |
44 | @Configuration
45 | public class TxMessageConfig {
46 | @Bean
47 | public TxMessageSendAdapter txMessageSendAdapter() {
48 | return new MyMessageSendAdapter();
49 | }
50 |
51 | @Bean
52 | public TxMessageRepository txMessageRepository(DataSource dataSource) {
53 | // your_table_name is your table name
54 | return new TxMessageRepository(dataSource, "your_table_name");
55 | }
56 |
57 | @Bean
58 | public SnowFlake snowFlake() {
59 | // pod unique
60 | return new SnowFlake(1,1);
61 | }
62 | }
63 |
64 | /**
65 | * your send adapter
66 | */
67 | class MyMessageSendAdapter implements TxMessageSendAdapter {
68 |
69 | // ...
70 |
71 | @Override
72 | public TxMessageSendResult send(TxMessage txMessage) {
73 | // your adapter logic
74 | TxMessageSendResult sendResult = new TxMessageSendResult();
75 | sendResult.setMsgId("xxx");
76 | sendResult.setSuccess(true);
77 | return sendResult;
78 | }
79 | }
80 | ```
81 |
82 | 3. enable and use it: save or try send
83 |
84 | set `spring.tx.message.send.enabled = true` to enable save and try send
85 |
86 | set `spring.tx.message.send.enabled = false` to not save and sync send
87 |
88 | ```java
89 | @Autowired
90 | TxMessageSender txMessageSender;
91 |
92 | public void sendMsg(TxMessage txMessage) {
93 | // save but don't retry send
94 | txMessageSender.batchSave(Collections.singletonList(txMessage));
95 | txMessageSender.saveAndTrySend(txMessage);
96 | txMessageSender.batchSaveAndTrySend(Collections.singletonList(txMessage));
97 | }
98 | ```
99 |
100 | 4. enabled use it: compensate send
101 |
102 | set `spring.tx.message.compensate.send.enabled = true` to enable compensate send
103 |
104 | set `spring.tx.message.compensate.interval.seconds` to customize compensate intervals
105 | ```java
106 | @Autowired
107 | TxMessageCompensateSender compensateSender;
108 |
109 | public void compensateSend() {
110 | // send with retry times = 4, when reaches max retry times , it will log.error and don't compensate send
111 | compensateSender.send(4);
112 |
113 | compensateSender.sendByNumberIgnoreStatus(1L);
114 | compensateSender.sendByMsgIdIgnoreStatus("xxx");
115 |
116 | }
117 | ```
118 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 | io.github.bty834
6 | spring-tx-message
7 | 0.0.1-SNAPSHOT
8 | spring-tx-message
9 | spring-tx-message
10 |
11 | 1.8
12 | UTF-8
13 | UTF-8
14 | 2.6.13
15 |
16 |
17 |
18 | org.springframework.boot
19 | spring-boot-autoconfigure
20 | provided
21 |
22 |
23 | org.springframework
24 | spring-context
25 | provided
26 |
27 |
28 | org.slf4j
29 | slf4j-api
30 | provided
31 |
32 |
33 | org.springframework
34 | spring-jdbc
35 | provided
36 |
37 |
38 | org.projectlombok
39 | lombok
40 | true
41 | provided
42 |
43 |
44 |
45 |
46 |
47 | org.springframework.boot
48 | spring-boot-dependencies
49 | ${spring-boot.version}
50 | pom
51 | import
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | org.apache.maven.plugins
60 | maven-compiler-plugin
61 | 3.8.1
62 |
63 | 1.8
64 | 1.8
65 | UTF-8
66 |
67 |
68 |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/src/main/java/io/github/bty834/springtxmessage/TxMessageCompensateSender.java:
--------------------------------------------------------------------------------
1 | package io.github.bty834.springtxmessage;
2 |
3 | public interface TxMessageCompensateSender {
4 |
5 | String COMPENSATE_ENABLED_KEY = "spring.tx.message.compensate.send.enabled";
6 | String COMPENSATE_INTERVAL_SECONDS = "spring.tx.message.compensate.interval.seconds";
7 |
8 | void send(int maxRetryTimes);
9 |
10 | void sendByNumberIgnoreStatus(Long number);
11 |
12 | void sendByMsgIdIgnoreStatus(String msgId);
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/java/io/github/bty834/springtxmessage/TxMessageSendAdapter.java:
--------------------------------------------------------------------------------
1 | package io.github.bty834.springtxmessage;
2 |
3 | import io.github.bty834.springtxmessage.config.Note;
4 | import io.github.bty834.springtxmessage.model.TxMessage;
5 | import io.github.bty834.springtxmessage.model.TxMessageSendResult;
6 |
7 | public interface TxMessageSendAdapter {
8 |
9 | @Note("do not catch your mq implementation's sending error")
10 | TxMessageSendResult send(TxMessage message);
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/java/io/github/bty834/springtxmessage/TxMessageSender.java:
--------------------------------------------------------------------------------
1 | package io.github.bty834.springtxmessage;
2 |
3 | import io.github.bty834.springtxmessage.config.Note;
4 | import io.github.bty834.springtxmessage.model.TxMessage;
5 | import java.util.List;
6 |
7 | public interface TxMessageSender {
8 |
9 | String ENABLED_KEY = "spring.tx.message.send.enabled";
10 |
11 | @Note("strictly send orderly , use batchSave but don't try send")
12 | void batchSave(List messages);
13 |
14 | void saveAndTrySend(TxMessage message);
15 |
16 | void batchSaveAndTrySend(List messages);
17 | }
18 |
--------------------------------------------------------------------------------
/src/main/java/io/github/bty834/springtxmessage/config/Note.java:
--------------------------------------------------------------------------------
1 | package io.github.bty834.springtxmessage.config;
2 |
3 | import java.lang.annotation.Documented;
4 | import java.lang.annotation.ElementType;
5 | import java.lang.annotation.Retention;
6 | import java.lang.annotation.RetentionPolicy;
7 | import java.lang.annotation.Target;
8 |
9 | @Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
10 | @Retention(RetentionPolicy.RUNTIME)
11 | @Documented
12 | public @interface Note {
13 |
14 | String value();
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/io/github/bty834/springtxmessage/config/TxMessageConfiguration.java:
--------------------------------------------------------------------------------
1 | package io.github.bty834.springtxmessage.config;
2 |
3 | import io.github.bty834.springtxmessage.support.DefaultTxMessageCompensateSender;
4 | import io.github.bty834.springtxmessage.support.DefaultTxMessageSender;
5 | import io.github.bty834.springtxmessage.TxMessageCompensateSender;
6 | import io.github.bty834.springtxmessage.support.TxMessageRepository;
7 | import io.github.bty834.springtxmessage.TxMessageSendAdapter;
8 | import io.github.bty834.springtxmessage.TxMessageSender;
9 | import io.github.bty834.springtxmessage.utils.SnowFlake;
10 | import javax.sql.DataSource;
11 | import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
12 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
13 | import org.springframework.context.annotation.Bean;
14 | import org.springframework.context.annotation.Configuration;
15 | import org.springframework.core.env.PropertyResolver;
16 |
17 |
18 | @Configuration
19 | public class TxMessageConfiguration {
20 |
21 | @Bean
22 | @ConditionalOnBean({TxMessageSendAdapter.class, TxMessageRepository.class, SnowFlake.class})
23 | public TxMessageSender txMessageSender(TxMessageSendAdapter adapter, TxMessageRepository txMessageRepository, PropertyResolver propertyResolver, SnowFlake snowFlake) {
24 | return new DefaultTxMessageSender(adapter, txMessageRepository, propertyResolver, snowFlake);
25 | }
26 |
27 | @Bean
28 | @ConditionalOnBean({TxMessageSendAdapter.class, TxMessageRepository.class})
29 | public TxMessageCompensateSender txMessageCompensateSender(TxMessageSendAdapter txMessageSendAdapter, TxMessageRepository txMessageRepository, PropertyResolver propertyResolver) {
30 | return new DefaultTxMessageCompensateSender(txMessageSendAdapter, txMessageRepository, propertyResolver);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/java/io/github/bty834/springtxmessage/model/SendStatus.java:
--------------------------------------------------------------------------------
1 | package io.github.bty834.springtxmessage.model;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Getter;
5 |
6 | @Getter
7 | @AllArgsConstructor
8 | public enum SendStatus {
9 |
10 | INIT(0),SUCCESS(100),FAILED(-2);
11 |
12 | private final int code;
13 |
14 |
15 | public static SendStatus fromCode(int code) {
16 | for (SendStatus status : values()) {
17 | if (status.getCode() == code) {
18 | return status;
19 | }
20 | }
21 | return null;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/java/io/github/bty834/springtxmessage/model/TxMessage.java:
--------------------------------------------------------------------------------
1 | package io.github.bty834.springtxmessage.model;
2 |
3 | import io.github.bty834.springtxmessage.config.Note;
4 | import lombok.AllArgsConstructor;
5 | import lombok.Builder;
6 | import lombok.Data;
7 | import lombok.NoArgsConstructor;
8 | import org.springframework.lang.Nullable;
9 |
10 | @Builder
11 | @Data
12 | @NoArgsConstructor
13 | @AllArgsConstructor
14 | public class TxMessage {
15 | private String topic;
16 | private String shardingKey;
17 | @Nullable
18 | @Note("send success callbacks insert this field")
19 | private String msgId;
20 | @Note("json string")
21 | private String content;
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/java/io/github/bty834/springtxmessage/model/TxMessagePO.java:
--------------------------------------------------------------------------------
1 | package io.github.bty834.springtxmessage.model;
2 |
3 | import io.github.bty834.springtxmessage.config.Note;
4 | import java.time.LocalDateTime;
5 | import lombok.Data;
6 | import lombok.ToString;
7 | import org.springframework.lang.Nullable;
8 |
9 | /**
10 | *
11 | CREATE TABLE `your_table_name`
12 | (
13 | `id` bigint NOT NULL AUTO_INCREMENT,
14 | `number` bigint NOT NULL ,
15 | `topic` varchar(255) NOT NULL,
16 | `sharding_key` varchar(255) DEFAULT NULL,
17 | `msg_id` varchar(255) DEFAULT NULL,
18 | `send_status` tinyint NOT NULL DEFAULT '0',
19 | `content` longtext NOT NULL,
20 | `retry_times` tinyint NOT NULL DEFAULT '0',
21 | `next_retry_time` datetime NOT NULL,
22 | `deleted` tinyint NOT NULL DEFAULT '0',
23 | `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
24 | `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
25 | PRIMARY KEY (`id`),
26 | KEY `idx_createtime` (`create_time`),
27 | KEY `idx_msgid` (`msg_id`),
28 | KEY `idx_updatetime` (`update_time`),
29 | KEY `idx_nextretrytime_retrytimes_sendstatus_deleted` (`send_status`,`next_retry_time`, `retry_times`, `deleted`)
30 | ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4
31 | *
32 | */
33 | @ToString
34 | @Data
35 | public class TxMessagePO {
36 | private Long id;
37 | private Long number;
38 | private String topic;
39 | private String shardingKey;
40 | @Nullable
41 | @Note("send success callbacks insert this field")
42 | private String msgId;
43 | private SendStatus sendStatus;
44 | @Note("json string")
45 | private String content;
46 | private Integer retryTimes;
47 | @Note("send failed set nextRetryTime")
48 | private LocalDateTime nextRetryTime;
49 | private Boolean deleted;
50 | private LocalDateTime createTime;
51 | private LocalDateTime updateTime;
52 |
53 | public static TxMessagePO convertFrom(TxMessage txMessage) {
54 | TxMessagePO po = new TxMessagePO();
55 | po.setContent(txMessage.getContent());
56 | po.setMsgId(txMessage.getMsgId());
57 | po.setTopic(txMessage.getTopic());
58 | po.setShardingKey(txMessage.getShardingKey());
59 | return po;
60 | }
61 |
62 | public TxMessage convertToTxMessage() {
63 | TxMessage txMessage = new TxMessage();
64 | txMessage.setContent(content);
65 | txMessage.setMsgId(msgId);
66 | txMessage.setTopic(topic);
67 | txMessage.setShardingKey(shardingKey);
68 | return txMessage;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/main/java/io/github/bty834/springtxmessage/model/TxMessageSendResult.java:
--------------------------------------------------------------------------------
1 | package io.github.bty834.springtxmessage.model;
2 |
3 | import lombok.Data;
4 |
5 | @Data
6 | public class TxMessageSendResult {
7 | private boolean success;
8 | private String msgId;
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/java/io/github/bty834/springtxmessage/support/DefaultTxMessageCompensateSender.java:
--------------------------------------------------------------------------------
1 | package io.github.bty834.springtxmessage.support;
2 |
3 | import io.github.bty834.springtxmessage.TxMessageCompensateSender;
4 | import io.github.bty834.springtxmessage.TxMessageSendAdapter;
5 | import io.github.bty834.springtxmessage.model.SendStatus;
6 | import io.github.bty834.springtxmessage.model.TxMessagePO;
7 | import io.github.bty834.springtxmessage.model.TxMessageSendResult;
8 | import java.time.LocalDateTime;
9 | import java.util.Collections;
10 | import java.util.List;
11 | import java.util.stream.Collectors;
12 | import lombok.RequiredArgsConstructor;
13 | import lombok.extern.slf4j.Slf4j;
14 | import org.springframework.core.env.PropertyResolver;
15 |
16 | @Slf4j
17 | @RequiredArgsConstructor
18 | public class DefaultTxMessageCompensateSender implements TxMessageCompensateSender {
19 |
20 | private final TxMessageSendAdapter txMessageSendAdapter;
21 |
22 | private final TxMessageRepository txMessageRepository;
23 |
24 | private final PropertyResolver propertyResolver;
25 |
26 | @Override
27 | public void send(int maxRetryTimes) {
28 | if (!propertyResolver.getProperty(COMPENSATE_ENABLED_KEY, Boolean.class, Boolean.FALSE)) {
29 | return;
30 | }
31 | Integer delaySeconds = propertyResolver.getProperty(COMPENSATE_INTERVAL_SECONDS, Integer.class, 6);
32 | List txMessages = txMessageRepository.queryReadyToSendMessages(maxRetryTimes, delaySeconds);
33 | List numbers = txMessages.stream()
34 | .filter(msg -> msg.getRetryTimes() >= maxRetryTimes)
35 | .map(TxMessagePO::getNumber)
36 | .collect(Collectors.toList());
37 | if (!numbers.isEmpty()) {
38 | log.error("reaches max retry times {}, numbers: {}", maxRetryTimes, numbers);
39 | }
40 | txMessages.removeIf(msg -> msg.getRetryTimes() >= maxRetryTimes);
41 | doSend(txMessages);
42 | }
43 |
44 | public void sendByNumberIgnoreStatus(Long number) {
45 | if (!propertyResolver.getProperty(COMPENSATE_ENABLED_KEY, Boolean.class, Boolean.FALSE)) {
46 | return;
47 | }
48 | TxMessagePO txMessage = txMessageRepository.queryByNumber(number);
49 | doSend(Collections.singletonList(txMessage));
50 | }
51 |
52 | public void sendByMsgIdIgnoreStatus(String msgId) {
53 | if (!propertyResolver.getProperty(COMPENSATE_ENABLED_KEY, Boolean.class, Boolean.FALSE)) {
54 | return;
55 | }
56 | TxMessagePO txMessage = txMessageRepository.queryByMsgId(msgId);
57 | doSend(Collections.singletonList(txMessage));
58 | }
59 |
60 | private void doSend(List txMessages){
61 | txMessages.forEach(msg -> {
62 | try {
63 | TxMessageSendResult sendResult = txMessageSendAdapter.send(msg.convertToTxMessage());
64 | if (!sendResult.isSuccess()) {
65 | throw new RuntimeException("Tx message compensate send failed: " + msg);
66 | }
67 | assert sendResult.getMsgId() != null;
68 | msg.setMsgId(sendResult.getMsgId());
69 | msg.setSendStatus(SendStatus.SUCCESS);
70 | txMessageRepository.updateByNumber(msg);
71 | } catch (Exception e) {
72 | log.error("compensate tx message failed :{}", msg, e);
73 | Integer interval = propertyResolver.getProperty(COMPENSATE_INTERVAL_SECONDS, Integer.class, 10);
74 | LocalDateTime nextRetryTime = LocalDateTime.now().plusSeconds(interval);
75 | msg.setNextRetryTime(nextRetryTime);
76 | msg.setRetryTimes(msg.getRetryTimes() + 1);
77 | msg.setSendStatus(SendStatus.FAILED);
78 | txMessageRepository.updateByNumber2(msg);
79 | }
80 | });
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/main/java/io/github/bty834/springtxmessage/support/DefaultTxMessageSender.java:
--------------------------------------------------------------------------------
1 | package io.github.bty834.springtxmessage.support;
2 |
3 | import io.github.bty834.springtxmessage.TxMessageSendAdapter;
4 | import io.github.bty834.springtxmessage.TxMessageSender;
5 | import io.github.bty834.springtxmessage.model.SendStatus;
6 | import io.github.bty834.springtxmessage.model.TxMessage;
7 | import io.github.bty834.springtxmessage.model.TxMessagePO;
8 | import io.github.bty834.springtxmessage.model.TxMessageSendResult;
9 | import io.github.bty834.springtxmessage.utils.SnowFlake;
10 | import io.github.bty834.springtxmessage.utils.TransactionUtil;
11 | import java.time.LocalDateTime;
12 | import java.util.Collections;
13 | import java.util.List;
14 | import java.util.stream.Collectors;
15 | import lombok.RequiredArgsConstructor;
16 | import lombok.extern.slf4j.Slf4j;
17 | import org.springframework.core.env.PropertyResolver;
18 |
19 | import static io.github.bty834.springtxmessage.TxMessageCompensateSender.COMPENSATE_INTERVAL_SECONDS;
20 |
21 | @Slf4j
22 | @RequiredArgsConstructor
23 | public class DefaultTxMessageSender implements TxMessageSender {
24 |
25 |
26 | private final TxMessageSendAdapter txMessageSendAdapter;
27 |
28 | private final TxMessageRepository txMessageRepository;
29 |
30 | private final PropertyResolver propertyResolver;
31 |
32 | private final SnowFlake snowFlake;
33 |
34 |
35 | @Override
36 | public void batchSave(List messages) {
37 | assert messages != null;
38 | if (!propertyResolver.getProperty(ENABLED_KEY, Boolean.class, Boolean.FALSE)) {
39 | return;
40 | }
41 | if (messages.isEmpty()) {
42 | return;
43 | }
44 | doBatchSave(messages);
45 | }
46 |
47 | private List doBatchSave(List messages) {
48 | List txMessagePOS = messages.stream().map(
49 | msg -> {
50 | TxMessagePO txMessagePO = TxMessagePO.convertFrom(msg);
51 | txMessagePO.setNumber(snowFlake.nextId());
52 | txMessagePO.setSendStatus(SendStatus.INIT);
53 | txMessagePO.setRetryTimes(0);
54 | Integer intervalSec = propertyResolver.getProperty(COMPENSATE_INTERVAL_SECONDS, Integer.class, 10);
55 | LocalDateTime nextRetryTime = LocalDateTime.now().plusSeconds(intervalSec);
56 | txMessagePO.setNextRetryTime(nextRetryTime);
57 | return txMessagePO;
58 | }
59 | ).collect(Collectors.toList());
60 | txMessageRepository.batchSave(txMessagePOS);
61 | return txMessagePOS;
62 | }
63 |
64 | @Override
65 | public void saveAndTrySend(TxMessage message) {
66 | assert message != null;
67 | if (propertyResolver.getProperty(ENABLED_KEY, Boolean.class, Boolean.FALSE)) {
68 | List txMessagePOS = doBatchSave(Collections.singletonList(message));
69 | TransactionUtil.executeAfterCommit(()-> trySend(txMessagePOS), (e)->{});
70 | return;
71 | }
72 | txMessageSendAdapter.send(message);
73 | }
74 |
75 | @Override
76 | public void batchSaveAndTrySend(List messages) {
77 | assert messages != null;
78 | if (propertyResolver.getProperty(ENABLED_KEY, Boolean.class, Boolean.FALSE)) {
79 | List txMessagePOS = doBatchSave(messages);
80 | TransactionUtil.executeAfterCommit(()-> trySend(txMessagePOS), (e)->{});
81 | return;
82 | }
83 | messages.forEach(txMessageSendAdapter::send);
84 | }
85 |
86 | private void trySend(List messages) {
87 | messages.forEach(msg -> {
88 | try {
89 | TxMessageSendResult sendResult = txMessageSendAdapter.send(msg.convertToTxMessage());
90 | if (!sendResult.isSuccess()) {
91 | throw new RuntimeException("Tx message send failed: " + msg);
92 | }
93 | assert sendResult.getMsgId() != null;
94 | msg.setMsgId(sendResult.getMsgId());
95 | msg.setSendStatus(SendStatus.SUCCESS);
96 | txMessageRepository.updateByNumber(msg);
97 | } catch (Exception e) {
98 | log.error("trySend tx message failed :{}", msg, e);
99 | Integer interval = propertyResolver.getProperty(COMPENSATE_INTERVAL_SECONDS, Integer.class, 10);
100 | LocalDateTime nextRetryTime = LocalDateTime.now().plusSeconds(interval);
101 | msg.setNextRetryTime(nextRetryTime);
102 | msg.setSendStatus(SendStatus.FAILED);
103 |
104 | txMessageRepository.updateByNumber2(msg);
105 | }
106 | });
107 | }
108 |
109 | }
110 |
--------------------------------------------------------------------------------
/src/main/java/io/github/bty834/springtxmessage/support/TxMessageRepository.java:
--------------------------------------------------------------------------------
1 | package io.github.bty834.springtxmessage.support;
2 |
3 | import io.github.bty834.springtxmessage.model.SendStatus;
4 | import io.github.bty834.springtxmessage.model.TxMessagePO;
5 | import java.sql.PreparedStatement;
6 | import java.sql.ResultSet;
7 | import java.sql.SQLException;
8 | import java.sql.Timestamp;
9 | import java.time.LocalDateTime;
10 | import java.util.List;
11 | import javax.sql.DataSource;
12 | import lombok.Setter;
13 | import org.springframework.jdbc.core.JdbcTemplate;
14 | import org.springframework.jdbc.core.ParameterizedPreparedStatementSetter;
15 | import org.springframework.jdbc.core.RowMapper;
16 |
17 | public class TxMessageRepository {
18 |
19 | private final JdbcTemplate jdbcTemplate;
20 |
21 | @Setter
22 | private Integer limit = 1000;
23 |
24 | @Setter
25 | private Integer insertBatchSize = 500;
26 |
27 | private static final RowMapper ROW_MAPPER = new TxMessageRowMapper();
28 |
29 |
30 | private final String queryByNumberSQL;
31 | private final String queryByMsgIdSQL;
32 | private final String queryReadyToSendMessagesSQL;
33 | private final String batchSaveSQL;
34 | private final String updateByNumberSQL;
35 | private final String updateByNumber2SQL;
36 |
37 | public TxMessageRepository(DataSource dataSource, String tableName) {
38 | jdbcTemplate = new JdbcTemplate(dataSource);
39 |
40 | queryByNumberSQL = "select * from " + tableName + " where number = ?";
41 | queryByMsgIdSQL = "select * from " + tableName + " where msg_id = ?";
42 | queryReadyToSendMessagesSQL = "select * from " + tableName + " where send_status in (?,?) and next_retry_time <= ? and retry_times <= ? and deleted = 0 limit ?";
43 | batchSaveSQL = " insert into " + tableName + " (number, topic, sharding_key, send_status, content, next_retry_time) value (?, ?, ?, ?, ?, ?)";
44 | updateByNumberSQL = "update " + tableName + " set send_status = ?, msg_id = ? where number = ? and deleted = 0";
45 | updateByNumber2SQL = "update " + tableName + " set send_status = ?, retry_times = ? , next_retry_time = ? where number = ? and deleted = 0";
46 | }
47 |
48 | public TxMessagePO queryByNumber(Long number) {
49 | return jdbcTemplate.queryForObject(queryByNumberSQL, ROW_MAPPER, number);
50 | }
51 |
52 | public TxMessagePO queryByMsgId(String msgId) {
53 | return jdbcTemplate.queryForObject(queryByMsgIdSQL, ROW_MAPPER, msgId);
54 | }
55 |
56 | public List queryReadyToSendMessages(int maxRetryTimes, int delaySeconds) {
57 | LocalDateTime nextRetryTimeLte = LocalDateTime.now().minusSeconds(delaySeconds);
58 | return jdbcTemplate.query(queryReadyToSendMessagesSQL, ROW_MAPPER, SendStatus.INIT.getCode(), SendStatus.FAILED.getCode(), nextRetryTimeLte, maxRetryTimes, limit);
59 | }
60 |
61 | // save并回填id
62 | public void batchSave(List messages) {
63 | if (messages.isEmpty()) {
64 | return;
65 | }
66 |
67 | jdbcTemplate.batchUpdate(batchSaveSQL, messages, insertBatchSize, new ParameterizedPreparedStatementSetter() {
68 | @Override
69 | public void setValues(PreparedStatement ps, TxMessagePO message) throws SQLException {
70 | ps.setLong(1, message.getNumber());
71 | ps.setString(2, message.getTopic());
72 | ps.setString(3, message.getShardingKey());
73 | ps.setInt(4, message.getSendStatus().getCode());
74 | ps.setString(5, message.getContent());
75 | ps.setTimestamp(6, Timestamp.valueOf(message.getNextRetryTime()));
76 | }
77 | });
78 | }
79 |
80 | public void updateByNumber(TxMessagePO msg) {
81 | jdbcTemplate.update(updateByNumberSQL, msg.getSendStatus().getCode(), msg.getMsgId(), msg.getNumber());
82 | }
83 |
84 | public void updateByNumber2(TxMessagePO message) {
85 | jdbcTemplate.update(updateByNumber2SQL, message.getSendStatus().getCode(), message.getRetryTimes(), message.getNextRetryTime(), message.getNumber());
86 | }
87 |
88 | private static class TxMessageRowMapper implements RowMapper {
89 | @Override
90 | public TxMessagePO mapRow(ResultSet rs, int rowNum) throws SQLException {
91 | TxMessagePO txMessage = new TxMessagePO();
92 | txMessage.setContent(rs.getString("content"));
93 | txMessage.setNumber(rs.getLong("number"));
94 | txMessage.setNextRetryTime(rs.getTimestamp("next_retry_time").toLocalDateTime());
95 | txMessage.setRetryTimes(rs.getInt("retry_times"));
96 | txMessage.setId(rs.getLong("id"));
97 | txMessage.setSendStatus(SendStatus.fromCode(rs.getInt("send_status")));
98 | txMessage.setUpdateTime(rs.getTimestamp("update_time").toLocalDateTime());
99 | txMessage.setCreateTime(rs.getTimestamp("create_time").toLocalDateTime());
100 | txMessage.setMsgId(rs.getString("msg_id"));
101 | txMessage.setTopic(rs.getString("topic"));
102 | txMessage.setShardingKey(rs.getString("sharding_key"));
103 | txMessage.setDeleted(rs.getBoolean("deleted"));
104 | return txMessage;
105 | }
106 | }
107 |
108 | }
109 |
110 |
111 |
112 |
--------------------------------------------------------------------------------
/src/main/java/io/github/bty834/springtxmessage/utils/SnowFlake.java:
--------------------------------------------------------------------------------
1 | package io.github.bty834.springtxmessage.utils;
2 |
3 |
4 | /**
5 | * twitter的snowflake算法 -- java实现
6 | *
7 | * @author beyond
8 | * @date 2016/11/26
9 | */
10 | public class SnowFlake {
11 |
12 |
13 | /**
14 | * 起始的时间戳
15 | */
16 | private final static long START_STMP = 1480166465631L;
17 |
18 | /**
19 | * 每一部分占用的位数
20 | */
21 | private final static long SEQUENCE_BIT = 12; //序列号占用的位数
22 | private final static long MACHINE_BIT = 5; //机器标识占用的位数
23 | private final static long DATACENTER_BIT = 5;//数据中心占用的位数
24 |
25 | /**
26 | * 每一部分的最大值
27 | */
28 | private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);
29 | private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
30 | private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);
31 |
32 | /**
33 | * 每一部分向左的位移
34 | */
35 | private final static long MACHINE_LEFT = SEQUENCE_BIT;
36 | private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
37 | private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;
38 |
39 | private long datacenterId; //数据中心
40 | private long machineId; //机器标识
41 | private long sequence = 0L; //序列号
42 | private long lastStmp = -1L;//上一次时间戳
43 |
44 | public SnowFlake(long datacenterId, long machineId) {
45 | if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {
46 | throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");
47 | }
48 | if (machineId > MAX_MACHINE_NUM || machineId < 0) {
49 | throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
50 | }
51 | this.datacenterId = datacenterId;
52 | this.machineId = machineId;
53 | }
54 |
55 | /**
56 | * 产生下一个ID
57 | *
58 | * @return
59 | */
60 | public synchronized long nextId() {
61 | long currStmp = getNewstmp();
62 | if (currStmp < lastStmp) {
63 | throw new RuntimeException("Clock moved backwards. Refusing to generate id");
64 | }
65 |
66 | if (currStmp == lastStmp) {
67 | //相同毫秒内,序列号自增
68 | sequence = (sequence + 1) & MAX_SEQUENCE;
69 | //同一毫秒的序列数已经达到最大
70 | if (sequence == 0L) {
71 | currStmp = getNextMill();
72 | }
73 | } else {
74 | //不同毫秒内,序列号置为0
75 | sequence = 0L;
76 | }
77 |
78 | lastStmp = currStmp;
79 |
80 | return (currStmp - START_STMP) << TIMESTMP_LEFT //时间戳部分
81 | | datacenterId << DATACENTER_LEFT //数据中心部分
82 | | machineId << MACHINE_LEFT //机器标识部分
83 | | sequence; //序列号部分
84 | }
85 |
86 | private long getNextMill() {
87 | long mill = getNewstmp();
88 | while (mill <= lastStmp) {
89 | mill = getNewstmp();
90 | }
91 | return mill;
92 | }
93 |
94 | private long getNewstmp() {
95 | return System.currentTimeMillis();
96 | }
97 |
98 | public static void main(String[] args) {
99 | SnowFlake snowFlake = new SnowFlake(2, 3);
100 |
101 | for (int i = 0; i < (1 << 12); i++) {
102 | System.out.println(snowFlake.nextId());
103 | }
104 |
105 | }
106 | }
--------------------------------------------------------------------------------
/src/main/java/io/github/bty834/springtxmessage/utils/TransactionUtil.java:
--------------------------------------------------------------------------------
1 | package io.github.bty834.springtxmessage.utils;
2 |
3 | import java.util.function.Consumer;
4 | import org.springframework.transaction.support.TransactionSynchronization;
5 | import org.springframework.transaction.support.TransactionSynchronizationAdapter;
6 | import org.springframework.transaction.support.TransactionSynchronizationManager;
7 |
8 | public class TransactionUtil {
9 |
10 | public static void executeAfterCommit(Runnable runnable , Consumer exceptionConsumer) {
11 | // 有事务,注册Synchronization,事务提交后执行
12 | if (TransactionSynchronizationManager.isSynchronizationActive()) {
13 | TransactionSynchronization transactionSynchronization = new TransactionSynchronizationAdapter() {
14 | @Override
15 | public void afterCommit() {
16 | try {
17 | runnable.run();
18 | } catch (Exception e) {
19 | exceptionConsumer.accept(e);
20 | }
21 | }
22 | };
23 | // 注册Synchronization
24 | TransactionSynchronizationManager.registerSynchronization(transactionSynchronization);
25 | return;
26 | }
27 | // 无事务直接执行
28 | runnable.run();
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/resources/META-INF/spring.factories:
--------------------------------------------------------------------------------
1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration = \
2 | io.github.bty834.springtxmessage.config.TxMessageConfiguration
--------------------------------------------------------------------------------
/src/main/resources/local_message.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `your_table_name`
2 | (
3 | `id` bigint NOT NULL AUTO_INCREMENT,
4 | `number` bigint NOT NULL ,
5 | `topic` varchar(255) NOT NULL,
6 | `sharding_key` varchar(255) DEFAULT NULL,
7 | `msg_id` varchar(255) DEFAULT NULL,
8 | `send_status` tinyint NOT NULL DEFAULT '0',
9 | `content` longtext NOT NULL,
10 | `retry_times` tinyint NOT NULL DEFAULT '0',
11 | `next_retry_time` datetime NOT NULL,
12 | `deleted` tinyint NOT NULL DEFAULT '0',
13 | `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
14 | `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
15 | PRIMARY KEY (`id`),
16 | KEY `number` (`number`),
17 | KEY `idx_createtime` (`create_time`),
18 | KEY `idx_msgid` (`msg_id`),
19 | KEY `idx_updatetime` (`update_time`),
20 | KEY `idx_nextretrytime_retrytimes_sendstatus_deleted` (`send_status`,`next_retry_time`, `retry_times`, `deleted`)
21 | ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4
22 |
--------------------------------------------------------------------------------