CAP原理
- C:Consistent,一致性。具体是指,操作成功以后所有的节点,在同一时间内,看到的数据是完全一致的,一致性说的就是数据一致性。
- A:Availability,可用性。值服务一致可用,在规定的时间内完成相应。
- P: Parition tolerance, 分区容错性。指分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供服务。
XA协议
- XA协议是由X/Open组织提出的分布式事务的规范。
- 由一个事务管理器(TM)和多个资源管理器(RM)组成。
- 两阶段提交:提交分两个阶段:prepare和commit。
– 保证数据的强一致性
– commit阶段出现问题,事务出现不一致,需人工处理
– 效率低下,性能与本地事务相差10倍
– MySql5.7及以上均支持XA协议
– MySql Connector/J 5.0以上支持XA协议
– Java系统中,数据源采用Atomikos
代码实现:
1.pom.xml 引入 starter
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
2.采用配置类的方式配置多数据源
@Configuration
@MapperScan(value = "com.example.xademo.db128.dao",sqlSessionFactoryRef = "sqlSessionFactoryBean128")
public class ConfigDb128 {
@Bean("db128")
public DataSource db131(){
MysqlXADataSource xaDataSource = new MysqlXADataSource();
xaDataSource.setUser("imooc");
xaDataSource.setPassword("Imooc@123456");
xaDataSource.setUrl("jdbc:mysql://192.168.163.128:3306/xa_128");
AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
atomikosDataSourceBean.setXaDataSource(xaDataSource);
return atomikosDataSourceBean;
}
@Bean("sqlSessionFactoryBean128")
public SqlSessionFactoryBean sqlSessionFactoryBean(@Qualifier("db128") DataSource dataSource) throws IOException {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
sqlSessionFactoryBean.setMapperLocations(resourceResolver.getResources("mybatis/db128/*.xml"));
return sqlSessionFactoryBean;
}
@Bean("xaTransaction")
public JtaTransactionManager jtaTransactionManager(){
UserTransaction userTransaction = new UserTransactionImp();
UserTransactionManager userTransactionManager = new UserTransactionManager();
return new JtaTransactionManager(userTransaction,userTransactionManager);
}
}
@Configuration
@MapperScan(value = "com.example.xademo.db129.dao",sqlSessionFactoryRef = "sqlSessionFactoryBean129")
public class ConfigDb129 {
@Bean("db129")
public DataSource db131(){
MysqlXADataSource xaDataSource = new MysqlXADataSource();
xaDataSource.setUser("imooc");
xaDataSource.setPassword("Imooc@123456");
xaDataSource.setUrl("jdbc:mysql://192.168.163.129:3306/xa_129");
AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
atomikosDataSourceBean.setXaDataSource(xaDataSource);
return atomikosDataSourceBean;
}
@Bean("sqlSessionFactoryBean129")
public SqlSessionFactoryBean sqlSessionFactoryBean(@Qualifier("db129") DataSource dataSource) throws IOException {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
sqlSessionFactoryBean.setMapperLocations(resourceResolver.getResources("mybatis/db129/*.xml"));
return sqlSessionFactoryBean;
}
}
- 注意:
DataSource
都要归AtomikosDataSourceBean
统一管理 - 注意:新建事务Jta的事物管理器
JtaTransactionManager
- 注意:
JtaTransactionManager
属于TM,两个DataSource
属于RM
3.测试
@Service
public class XAService {
@Resource
private XA129Mapper xa129Mapper;
@Resource
private XA128Mapper xa128Mapper;
@Transactional(transactionManager = "xaTransaction")
public void testXA(){
XA129 xa129 = new XA129();
xa129.setId(1);
xa129.setName("xa_129");
xa129Mapper.insert(xa129);
XA128 xa128 = new XA128();
xa128.setId(1);
xa128.setName("xa_128");
xa128Mapper.insert(xa128);
}
}
@SpringBootTest
@RunWith(value = SpringRunner.class)
public class XaDemoApplicationTests {
@Autowired
private XAService xaService;
@Test
public void contextLoads() {
}
@Test
public void testXA(){
xaService.testXA();
}
}
Mycat
mycat 版本 1.6 以后是支持事务的,主要体现在server.xml
里面:
<!--分布式事务开关,0为不过滤分布式事务,1为过滤分布式事务(如果分布式事务内只涉及全局表,则不过滤),2为不过滤分布式事务,但是记录分布式事务日志-->
<property name="handleDistributedTransactions">0</property>
- 0: 表示开启分布式事务
- 1: 表示不开启分布式事务
1.在Navicat中执行插入语句需:
SET autocommit = 0;
set xa = on;
INSERT INTO `user`(id,username) VALUES(1,'奇数'),(2,'偶数123456');
commit;
2.在项目中执行直接连接mycat即可,和直接连接mysql是一样的,配置数据源,执行程序。
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.url=jdbc:mysql://192.168.163.129:8066/user?serverTimezone=Asia/Shanghai&useSSL=false
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void testUser(){
User user1 = new User();
user1.setId(1);
user1.setUsername("奇数");
userMapper.insert(user1);
User user2 = new User();
user2.setId(2);
user2.setUsername("偶数123456");
userMapper.insert(user2);
}
Sharding JDBC
sharding JDBC 默认开启了分布式事务
事务补偿机制
什么是事务补偿机制?
针对每个操作都要注册一个与其对应的补偿(撤销)操作。
在执行失败的时候,调用补偿操作,撤销之前的操作。
基于本地消息表的最终一致性方案
- 采用Base原理,保证事务的最终一致性方案。
- 在一致性方面,允许一段时间内的不一致,但最终会一致。
- 在实际的系统和当中,要根据具体情况,判断是否采用。
原理:
1.基于本地消息表的方案中,将本事务外操作,记录在消息表中。
2. 其他事务提供操作接口。
3. 定时任务轮询本地消息表,将未执行的消息发送给操作接口。
4. 操作接口处理成功返回成功标识,处理失败返回失败标识。
5. 定时任务接到标识,更新消息的状态。
6. 定时任务按照一定的周期反复执行。
7. 对于屡次失败的消息,可以设置最大失败次数。
8. 超过最大的失败次数的消息,不在进行接口调用。
9. 等待人工补偿。
@RestController
public class PaymentController {
@Autowired
private PaymentService paymentService;
@RequestMapping("payment")
public String payment(int userId , int orderId, BigDecimal amount){
int payment = paymentService.payment(userId, orderId, amount);
return "支付结果 :" + payment;
}
}
@Service
public class PaymentService {
@Resource
private AccountAMapper accountAMapper;
@Resource
private PaymentMsgMapper paymentMsgMapper;
/**
* 支付接口
* @param userId
* @param orderId
* @param amount
* @return 1:用户不存在;2: 余额不足;0:代表成功。
*
*/
@Transactional(transactionManager = "tm128")
public int payment(int userId , int orderId, BigDecimal amount){
//支付操作
AccountA accountA = accountAMapper.selectByPrimaryKey(userId);
if(accountA == null){
return 1;
}
if(accountA.getBalance().compareTo(amount) < 0){
return 2;
}
accountA.setBalance(accountA.getBalance().subtract(amount));
accountAMapper.updateByPrimaryKey(accountA);
PaymentMsg paymentMsg = new PaymentMsg();
paymentMsg.setOrderId(orderId);
paymentMsg.setStatus(0);//未发送
paymentMsg.setFalureCnt(0);
paymentMsg.setCreateTime(new Date());
paymentMsg.setCreateUser(userId);
paymentMsg.setUpdateTime(new Date());
paymentMsg.setUpdateUser(userId);
paymentMsgMapper.insertSelective(paymentMsg);
return 0;
}
}
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@RequestMapping("handleOrder")
public String handleOrder(int orderId){
try{
int result = orderService.handleOrder(orderId);
if(result == 0){
return "success";
}
return "fail";
}catch (Exception e){
return "fail";
}
}
}
@Service
public class OrderService {
@Resource
private OrderMapper orderMapper;
/**
* 订单回调接口
* @param orderId
* @return 1: 订单不存在;0: 成功。
*/
@Transactional(transactionManager = "tm129")
public int handleOrder(int orderId){
Order order = orderMapper.selectByPrimaryKey(orderId);
if(order == null){
return 1;
}
order.setOrderStatus(1);//已支付
order.setUpdateTime(new Date());
order.setUpdateUser(0); //系统更新
orderMapper.updateByPrimaryKey(order);
return 0;
}
}
@Service
public class OrderScheduler {
@Resource
private PaymentMsgMapper paymentMsgMapper;
@Scheduled(cron = "0/10 * * * * ?")
public void orderNotify() throws IOException {
System.out.println("================================");
PaymentMsgExample paymentMsgExample = new PaymentMsgExample();
paymentMsgExample.createCriteria().andStatusEqualTo(0);//未发送
List<PaymentMsg> paymentMsgs = paymentMsgMapper.selectByExample(paymentMsgExample);
if(paymentMsgs == null || paymentMsgs.isEmpty()){
return;
}
for (PaymentMsg paymentMsg : paymentMsgs) {
Integer orderId = paymentMsg.getOrderId();
CloseableHttpClient httpClient = HttpClientBuilder.create().build();
HttpPost httpPost = new HttpPost("http://localhost:8080/handleOrder");
NameValuePair orderPair = new BasicNameValuePair("orderId",orderId+"");
List<NameValuePair> list = new ArrayList<>();
list.add(orderPair);
HttpEntity httpEntity = new UrlEncodedFormEntity(list,"utf-8");
httpPost.setEntity(httpEntity);
CloseableHttpResponse response = httpClient.execute(httpPost);
String s = EntityUtils.toString(response.getEntity());
if("success".equals(s)){
paymentMsg.setStatus(1);//发送成功
paymentMsg.setUpdateTime(new Date());
paymentMsg.setUpdateUser(0);//系统更新
paymentMsgMapper.updateByPrimaryKey(paymentMsg);
}else{
Integer falureCnt = paymentMsg.getFalureCnt();
falureCnt++;
paymentMsg.setFalureCnt(falureCnt);
if(falureCnt > 5){
paymentMsg.setStatus(2);//失败
paymentMsg.setUpdateTime(new Date());
paymentMsg.setUpdateUser(0);//系统更新
paymentMsgMapper.updateByPrimaryKey(paymentMsg);
}
paymentMsg.setUpdateTime(new Date());
paymentMsg.setUpdateUser(0);//系统更新
paymentMsgMapper.updateByPrimaryKey(paymentMsg);
}
}
}
}
基于MQ的最终一致性方案
- 原理与本地消息表类似。
- 不同点:本地消息表改为MQ。
- 定时任务改为MQ的消费者。
- 优点:不依赖定时任务,基于MQ更高效,更可靠。
- 适合公司内的系统,不同公司之间无法基于MQ,本地消息表更合适。
@Configuration
public class RocketMQConfig {
@Bean(initMethod = "start", destroyMethod = "shutdown")
public DefaultMQProducer producer(){
DefaultMQProducer producer = new
DefaultMQProducer("paymentGroup");
// Specify name server addresses.
producer.setNamesrvAddr("localhost:9876");
return producer;
}
@Bean(initMethod = "start",destroyMethod = "shutdown")
public DefaultMQPushConsumer consumer(@Qualifier("messageListener") MessageListenerConcurrently messageListener) throws MQClientException {
// Instantiate with specified consumer group name.
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("paymentConsumerGroup");
// Specify name server addresses.
consumer.setNamesrvAddr("localhost:9876");
// Subscribe one more more topics to consume.
consumer.subscribe("payment", "*");
// Register callback to execute on arrival of messages fetched from brokers.
consumer.registerMessageListener(messageListener);
return consumer;
}
}
@Component("messageListener")
public class ChangeOrderStatus implements MessageListenerConcurrently {
@Resource
private OrderMapper orderMapper;
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list,
ConsumeConcurrentlyContext consumeConcurrentlyContext) {
if(list == null || list.isEmpty()){
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
for (MessageExt messageExt : list) {
String orderId = messageExt.getKeys();
byte[] body = messageExt.getBody();
String msg = new String(body);
System.out.println("msg = " + msg);
Order order = orderMapper.selectByPrimaryKey(Integer.parseInt(orderId));
if(order == null){
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
try{
order.setOrderStatus(1);//已支付
order.setUpdateTime(new Date());
order.setUpdateUser(0); //系统更新
orderMapper.updateByPrimaryKey(order);
}catch (Exception e){
e.printStackTrace();
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
}
/**
* 支付接口(消息队列)
* @param userId
* @param orderId
* @param amount
* @return 1:用户不存在;2: 余额不足;0:代表成功。
*
*/
@Transactional(transactionManager = "tm128",rollbackFor = Exception.class)
public int paymentMQ(int userId , int orderId, BigDecimal amount) throws Exception {
//支付操作
AccountA accountA = accountAMapper.selectByPrimaryKey(userId);
if(accountA == null){
return 1;
}
if(accountA.getBalance().compareTo(amount) < 0){
return 2;
}
accountA.setBalance(accountA.getBalance().subtract(amount));
accountAMapper.updateByPrimaryKey(accountA);
Message message = new Message();
message.setTopic("payment");
message.setKeys(orderId+"");
message.setBody("订单已支付".getBytes());
try {
SendResult result = producer.send(message);
if(result.getSendStatus() == SendStatus.SEND_OK){
return 0;
}else{
throw new Exception("消息发送失败");
}
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}