6000字搞不懂大型网站架构技术细节:后端架构数据库分布式事务?

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群版 2核4GB 100GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: 上节中讨论的数据库事务是解决“单个数据库数据不一致”的问题,而在一些具有规模的网站系统当中,数据库往往不止一个,一旦出现多个数据库,则会出现多数据库的数据不一致问题。多个数据库的数据不一致问题一般有两种场景,如图4.76所示。

分布式事务

上节中讨论的数据库事务是解决“单个数据库数据不一致”的问题,而在一些具有规模的网站系统当中,数据库往往不止一个,一旦出现多个数据库,则会出现多数据库的数据不一致问题。

多个数据库的数据不一致问题一般有两种场景,如图4.76所示。

网络异常,图片无法展示
|

图4.76 多数据库的数据不一致场景

场景一,由于“应用拆分”和“数据库分离”造成的多数据库数据不一致问题,例如,用户余额存放在“资金”数据库中,优惠券存放在“优惠券”数据库,当发生交易时,会出现扣减余额成功,但扣减优惠券失败的情况;场景二,当数据库中的数据个数超过千万级时,一般都需要进行分库,这也会造成多数据库的数据不一致问题,例如,用户1的余额存在数据库A中,而用户2的余额存在数据库B中,当用户1向用户2转账时,会出现用户1扣除金额成功,而用户2增加金额失败的情况。

解决“多数据库的数据不一致问题”的方式被称为分布式事务,确切地说,分布式事务不是某种具体的方式,而是解决“多数据库的数据不一致问题”的方式的统称,分布式事务的实现方式本身是开放的。

说明:关于分布式事务的相关理论有CAP原则和BASE理论;关于分布式事务的相关算法有2PC、3PC、TCC及本地消息表等。

分布式事务是行业内一大难题,在选取分布式事务具体方案之前,需要分析清楚是否真的需要分布式事务。很多时候,造成场景一的原因,是由于过度设计造成的,即在系统内部划分出很多没必要的独立模块。例如,图4.76中所举的例子,优惠券本身可以算是资金的一部分,如果把资金和优惠券合并成一个模块,则不需要分布式事务。

注意:以下介绍的分布式事务的解决方法都不是唯一方法,具体实施需要根据实际情况而定。

1.XA事务

XA(eXtended Architecture)是由X/Open组织提出的分布式事务的规范,XA事务能较好地解决“多数据库的数据不一致”问题。目前,比较流行的数据库(如Oracle、MySQL等)都支持XA事务。

注意:Oracle执行XA事务的性能要优于MySQL,另外,MySQL最好选用5.7或之后的版本,因为MySQL 5.7之前的版本对XA事务的支持都是有缺陷的。

XA事务可以简单地理解为多个数据库同时执行数据库事务,但与执行普通数据库事务不同的是,最后的提交阶段需要先检查所有事务的状态(是否允许提交)后才能提交。第三方软件使用数据库XA事务的流程如图4.77所示。以Java为例,通过JDBC使用XA事务的代码如代码4.55所示。

网络异常,图片无法展示
|

图4.77 第三方软件使用数据库XA事务的流程

代码4.55 通过JDBC使用XA事务

//连接数据库1,并获取资源管理器对象rm_1

Connection conn_1 = DriverManager.getConnection(

"jdbc:mysql://ip:port/xxx",

"userName",

"password");

XAConnection xaConn_1 = new MysqlXAConnection((com.mysql.jdbc.Connection)

conn_1, false);

XAResource rm_1 = xaConn_1.getXAResource();//连接数据库2,并获取资源管理器对象rm_2

Connection conn_2 = DriverManager.getConnection(

"jdbc:mysql://ip:port/xxx",

"userName",

"password");

XAConnection xaConn_2 = new MysqlXAConnection((com.mysql.jdbc.Connection)

conn_2, false);

XAResource rm_2 = xaConn_2.getXAResource();

//设置全局事务ID

//xxx可以用UUID.randomUUID().toString()生成

byte[] gtrid = "xxx".getBytes();

try{

//操作数据库1

//生成数据库1的事务ID,gtrid为全局事务ID,bqual_1为分支限定符

byte[] bqual_1 = "db_1".getBytes();

Xid xid_1 = new MysqlXid(gtrid, bqual_1, 1);

//启动数据库1的事务

rm_1.start(xid_1, XAResource.TMNOFLAGS);

//使用conn_1执行SQL语句(数据库1执行SQL语句)

//SQL语句执行结束,迁移事务状态

rm_1.end(xid_1, XAResource.TMSUCCESS);

//操作数据库2

//生成数据库2的事务ID,bqual_2需要与数据库1的值有所区别

byte[] bqual_2 = "db_2".getBytes();

Xid xid_2 = new MysqlXid(gtrid, bqual_2, 1);

//启动数据库2的事务

rm_2.start(xid_2, XAResource.TMNOFLAGS);

//使用conn_2执行SQL语句(数据库2执行SQL语句)

//SQL语句执行结束,迁移事务状态

rm_2.end(xid_2, XAResource.TMSUCCESS);

//两段提交

//准备阶段,获取两个数据库的事务状态

int prepare_1 = rm_1.prepare(xid_1);

int prepare_2 = rm_2.prepare(xid_2);

//提交阶段,根据两个事务的状态决定提交还是回滚

if (prepare_1 == XAResource.XA_OK && prepare_2 == XAResource.XA_OK) {

rm_1.commit(xid_1, false);

rm_2.commit(xid_2, false);

} else { //一个数据库事务存在问题,则回滚

rm_1.rollback(xid_1);

rm_2.rollback(xid_2);

}

} catch(XAException e) {

//发生错误,回滚事务

rm_1.rollback(xid_1);rm_2.rollback(xid_2);

}

XA事务在使用上是简单的,但是由于XA事务是同时对多个数据库执行数据库事务,因此会同时浪费多个数据库的性能。在大型网站系统当中,XA事务一般是不被提倡的,因为大型网站系统需要应对高并发场景,在高并发压力下,XA事务往往会大量阻塞任务,从而引发超时等异常。不过,在一个大型网站系统中,并发压力的分布往往是不均等的,也就是说,存在并发压力不大的模块,而在这些模块中使用XA事务也是可以的。因此,XA事务的好处是使用简单,但不适合用于并发压力大的功能模块。

2.JTA

JTA(Java Transaction API)是Java的事务管理框架。通过使用JTA,开发者可以更简单地实现XA事务。值得一提的是,JTA只是简化了编码,并没有改变XA事务性能差的状况。下面以Spring Boot为例,介绍使用JTA实现XA事务的过程。

说明:JTA实际上只是提供了事务管理的统一接口,它本身不负责具体的实现,具体的实现交由Atomikos或JOTM等事务管理器完成。

(1)引入JTA依赖包。需要在工程配置文件(build.gradle)中添加JTA的依赖包,如代码4.56所示,其中,选用Atomikos作用具体的事务管理器,数据库操作框架选用JDBC Template。

代码4.56 在build.gradle中添加JTA依赖包

dependencies {

//在dependencies中添加JTA的依赖包,选用Atomikos作为具体的事务管理器

implementation 'org.springframework.boot:spring-boot-starter-jta

atomikos'

//添加数据库操作框架依赖包,这里选用JDBC Template

implementation 'org.springframework.boot:spring-boot-starter-jdbc'

implementation 'com.alibaba:druid:1.0.26' //数据库连接池依赖

runtimeOnly '
mysql:mysql-connector-java' //MySQL驱动依赖

}

添加完依赖包后,需要同步工程配置。JTA的依赖包在同步工程配置后才会被下载和引入。在IntelliJ IDEA中,只需要单击“同步”按钮即可同步工程配置,如图4.78所示。

网络异常,图片无法展示
|

图4.78 在IntelliJ IDEA中同步build.gradle配置

(2)配置数据库信息。配置数据库连接信息需要在后端应用程序的配置文件(默认是application.properties)中设置。与平常配置数据库不同的是,这里需要配置两个数据库连接信息,如代码4.57所示,其中,数据库1通过Spring Boot提供的默认字段配置,数据库2通过自定义字段配置。

说明:默认情况下,Spring Boot只提供一个数据库的连接配置,如果需要连接多个数据库,则需要通过额外代码完成多个数据库的连接。代码4.57 在配置文件中添加数据库连接信息

#配置说明可参考4.4.4小节中的代码4.50

#数据库1的连接信息,通过Spring Boot的默认字段配置,也可通过自定义字段配置

spring.datasource.jdbc-url=jdbc:mysql://ip:port/xxx

spring.datasource.username=root

spring.datasource.password=password

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

spring.datasource.max-active=20

spring.datasource.max-idle=8

spring.datasource.initial-size=10

#数据库2的连接信息,通过自定义字段配置,dao.extradb为自定义字段

dao.extradb.jdbc-url=jdbc:mysql://ip:port/xxx

dao.extradb.username=root

dao.extradb.password=password

dao.extradb.driver-class-name=com.mysql.cj.jdbc.Driver

dao.extradb.type=com.alibaba.druid.pool.DruidDataSourcedao.extradb.max-active=20

dao.extradb.max-idle=8

dao.extradb.initial-size=10

配置文件虽然配置了多个数据库连接信息,但默认情况下Spring Boot只接受一个数据库的设置,因此需要添加额外代码(新建一个Java文件)关联这些数据库连接信息,如代码4.58所示。

说明:代码4.58是全局设置,不需要被其他文件引用,存放位置最好在Dao层内,以方便开发者查阅。

代码4.58 在配置文件中添加数据库连接信息

package com.example.demo.dao.config;

import org.springframework.beans.factory.annotation.Qualifier;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.boot.context.properties.ConfigurationProperties;

import org.springframework.boot.jdbc.DataSourceBuilder;

import org.springframework.context.annotation.Primary;

import org.springframework.jdbc.core.JdbcTemplate;

import javax.sql.DataSource;

@Configuration

public class DataBaseConfig {

//关联数据库1的连接信息,并设置为默认连接

//设置标识,关联函数DataBaseTemplate1()中的参数

@Bean(name="dataBaseConfig_1")

@Primary //默认数据库标识

//spring.datasource为数据库1的连接信息字段

@ConfigurationProperties(prefix="spring.datasource")

DataSource DataBaseConfig1(){

return DataSourceBuilder.create().build();

}

@Bean(name="databaseTemplate_1")

@Primary //默认数据库标识

public JdbcTemplate DataBaseTemplate1(@Qualifier("dataBaseConfig_1")

DataSource data){

return new JdbcTemplate(data);

}

//关联数据库2的连接信息

//设置标识,关联函数DataBaseTemplate2()中的参数

@Bean(name="dataBaseConfig_2")//dao.extradb为数据库2的自定义连接信息字段

@ConfigurationProperties(prefix = "dao.extradb")

DataSource DataBaseConfig2(){

return DataSourceBuilder.create().build();

}

@Bean(name="databaseTemplate_2")

public JdbcTemplate DataBaseTemplate2(@Qualifier("dataBaseConfig_2")

DataSource data){

return new JdbcTemplate(data);

}

}

(3)在Dao层中操作多个数据库,如代码4.59所示。在实际编码中,对不同数据库的操作最好分为不同的文件。

代码4.59 在Dao层中操作多个数据库

package com.example.demo.dao;

//引用JDBCTemplate的类

import org.springframework.jdbc.core.JdbcTemplate;

import … //省略其他引用的类

@Repository("TestDao")

public class TestDao {

//获取数据库1(默认数据库)的JdbcTemplate对象,此对象会被自动注入

@Autowired

private JdbcTemplate jdbcTemplate_1;

//获取数据库2的JdbcTemplate对象,databaseTemplate_2为代码4.58中的标识,

此对象会被自动注入

@Autowired

@Qualifier("databaseTemplate_2")

private JdbcTemplate jdbcTemplate_2;

//在数据库1增加一条记录

public String Create_1(String value1, String value2){

try{

//SQL语句,?为占位符,后续通过参数替换

String sqlString = "INSERT INTO formName VALUES (?, ?)";

//执行SQL语句

int result = jdbcTemplate.update(sqlString, value1, value2);

if(result > 0){

return "success";

}else {

return "fail";

}

}catch(Exception e){return "fail";

}

}

//在数据库2增加一条记录

public String Create_2(String value1, String value2){

try{

String sqlString = "INSERT INTO formName VALUES (?, ?)";

int result = jdbcTemplate.update(sqlString, value1, value2);

if(result > 0){

return "success";

}else {

return "fail";

}

}catch(Exception e){

return "fail";

}

}

(4)在业务层(Service层)中标记需要使用数据库事务的方法,如代码4.60所示,其中,@Transactional为XA事务标记。

说明:代码4.60中的@Transactional标识与数据库事务的标识相同,但其本质上是XA事务。

代码4.60 在Service层中添加事务标记

@Service("XXX")

public class XXXService {

@Resource(name = "TestDao")

private TestDao _testDao;

//标记数据库事务,当异常发生时,会自动撤销所有数据库操作。函数中不能用catch捕获

异常

@Transactional

public JSONObject ServiceFunction(JSONObject requestParam, JSONObject

returnParam){

//调用代码4.59中的函数1(操作数据库1)

_testDao.Create_1(…);

//调用代码4.59中的函数2(操作数据库2)

_testDao.Create_1 (…);

return returnParam;

}

}

3.本地消息表

当多个数据库操作分别处在不同的程序中却又需要保持数据一致性时(图4.76中的场景一),一般采用“本地消息表”实现分布式事务。“本地消息表”这个方案的核心是将分布式事务拆分成多个数据库事务进行处理,并通过额外的检查机制确保多个数据库事务都正常完成,以达到数据最终一致的效果。“本地消息表”方案的工作原理如图4.79所示。其中,检查程序可以是程序1本身,本地消息表的实体可以是本地文件、数据库中的表等。

网络异常,图片无法展示
|

图4.79 “本地消息表”方案的工作原理

“本地消息表”方案是解决分布式事务的一种思路,而其具体的实现是开放的,针对不同的应用场景或软件形态会有不同的实现方式。针对后端应用程序而言,可参照图4.80所示的工作流程,其中,检查程序最好是独立的一个程序,本地消息表一般是数据库中的表(可以为数据库1或数据库2中的表,也可以是独立数据库中的表)。

网络异常,图片无法展示
|

图4.80 后端应用程序采用“本地消息表”实现分布式事务的工作流程

说明:图4.80中的检查程序也可以是后端应用程序1中的定时任务,但是后端应用程序1可能被部署在多个服务器上,如果是这样的话,则需要使用Quartz等分布式定时器框架,以避免发生定时任务被多次执行的情况。

“本地消息表”方案的实现在实际编码中比较麻烦,而且会增加后端结构的复杂性。而性能方面,一般来说“本地消息表”方案更胜一筹。但是,因为“本地消息表”方案的具体实现是五花八门的,而且其内部可能会用到XA事务,所以很难评定与单纯使用XA事务的性能对比。

4.其他

除了“XA事务”和“本地消息表”这两种分布式事务解决方案以外,还有很多其他解决思路或方案,如2PC(两段式提交,XA事务是其中一种实现)、3PC(三段式提交)、基于消息队列的解决方案及TCC(事务补偿)方案等。但是,无论采用哪种方案,分布式事务都会增加系统复杂度和限制数据库性能,所以架构设计应该尽量避开分布式事务。如果不能完全避开分布式事务,则需要对系统内的分布式事务提供统一的规范,以防止“五花八门”的分布式事务解决方案出现在网站系统当中(限制系统混乱度)。

本文给大家讲解的内容是大型网站架构的技术细节:后端架构数据库--分布式事务

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
3天前
|
存储 NoSQL 关系型数据库
现代数据库技术的演进与应用
本文探讨了现代数据库技术在信息时代的关键作用与发展趋势。从传统关系型数据库到新兴的NoSQL和分布式数据库,我们将深入剖析它们的优缺点及在各种应用场景中的实际运用。
|
1天前
|
弹性计算 运维 Kubernetes
探索后端开发的未来:微服务架构与容器化技术
在数字化时代的浪潮中,后端开发正经历着前所未有的变革。微服务架构的兴起和容器化技术的普及,不仅重新定义了软件的开发、部署和管理方式,还为后端开发带来了新的挑战和机遇。本文将深入探讨微服务架构和容器化技术如何影响后端开发的未来,通过数据支撑和逻辑推理,揭示这些技术趋势背后的科学原理和实际应用价值。
|
20小时前
|
运维 Kubernetes Docker
容器化技术在微服务架构中的应用
【7月更文挑战第3天】容器化技术在微服务架构中的应用,为现代应用的开发、部署和运维带来了革命性的变化。通过容器化,我们可以实现服务的快速部署、独立运行和高效扩展,同时提高资源的利用率和系统的可维护性。随着容器技术的不断发展和完善,相信它将在未来的软件开发中发挥更加重要的作用。
|
1天前
|
消息中间件 Java 开发者
Java中实现事件驱动架构的异步通信技术
Java中实现事件驱动架构的异步通信技术
|
2天前
|
运维 Cloud Native Devops
云原生架构的演进与实践:面向未来的企业技术战略
在数字化转型的浪潮中,云原生架构已成为推动企业技术创新和业务敏捷性的核心力量。本文旨在深入探讨云原生架构的发展历程、关键技术组件以及在实际应用中的效益与挑战。通过分析来自全球不同行业的实证数据和案例研究,文章揭示云原生技术如何助力企业实现资源的高效利用、应用的快速迭代和系统的弹性扩展。同时,结合最新的研究成果和行业报告,为读者提供一套系统化的云原生采纳指南和战略规划建议,以期帮助企业构建面向未来的技术体系,并在激烈的市场竞争中保持领先地位。
20 0
|
2天前
|
Java 数据库连接 Apache
解决Java中数据库连接泄露的技术
解决Java中数据库连接泄露的技术
|
20小时前
|
设计模式 弹性计算 监控
后端开发中的微服务架构:优势、挑战与实施策略
在现代软件开发中,微服务架构已成为一种流行的设计模式,特别是在后端开发领域。该架构风格通过将应用程序分解为一组小型、松耦合的服务,旨在提升可维护性、可扩展性和敏捷性。本文深入探讨了微服务架构的关键优势,面临的主要挑战,以及成功实施微服务的策略。通过引用业界案例和最新研究,文章提供了对微服务架构综合理解的视角,并讨论了如何在不断变化的技术环境中保持其有效性。
|
20小时前
|
设计模式 安全 持续交付
探索微服务架构下的后端开发实践
在现代软件开发领域,微服务架构已成为一种流行的设计模式,它通过将应用程序分解为一组小的服务来促进敏捷开发和可扩展性。本文深入探讨了微服务架构的核心概念、技术选型、数据一致性挑战以及安全性考虑,旨在为后端开发人员提供一份全面的微服务开发指南。文章结合最新的研究成果和业界最佳实践,分析了微服务架构的优势和面临的挑战,并提出了相应的解决方案。读者将了解到如何在实际项目中应用微服务原则,以及如何克服实施过程中的技术和组织障碍。
|
1天前
|
缓存 监控 负载均衡
微服务架构下的API网关设计与实现
在分布式系统和微服务架构中,API网关扮演着至关重要的角色。它不仅是服务的单一入口点,还负责请求的路由、负载均衡、认证授权、限流熔断等关键功能。本文将深入探讨API网关的设计理念、核心组件以及实现策略,旨在为开发者提供一套完整的API网关解决方案。通过分析现代微服务架构的需求,结合最新的技术趋势,我们将展示如何构建一个高效、可靠且易于维护的API网关。
11 0
|
1天前
|
存储 设计模式 监控
后端开发中的微服务架构实践与挑战
在数字化时代背景下,微服务架构作为现代软件工程的典范,被广泛应用于后端开发领域。本文将深入探讨微服务架构的核心概念、实施策略及其面临的主要挑战,同时提供一系列针对性的解决方案和最佳实践。通过引用最新的研究成果和行业案例,文章旨在为后端开发者提供一个全面的微服务架构指南,帮助他们在构建和维护复杂系统时做出明智的决策。
7 1