├── .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 | --------------------------------------------------------------------------------