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

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
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
相关文章
|
18天前
|
负载均衡 网络协议 数据库
选择适合自己的数据库多实例负载均衡技术
【10月更文挑战第23天】选择适合自己的数据库多实例负载均衡技术需要全面考虑多种因素。通过深入的分析和评估,结合自身的实际情况,能够做出明智的决策,为数据库系统的高效运行提供有力保障。
103 61
|
13天前
|
存储 SQL Apache
Apache Doris 开源最顶级基于MPP架构的高性能实时分析数据库
Apache Doris 是一个基于 MPP 架构的高性能实时分析数据库,以其极高的速度和易用性著称。它支持高并发点查询和复杂分析场景,适用于报表分析、即席查询、数据仓库和数据湖查询加速等。最新发布的 2.0.2 版本在性能、稳定性和多租户支持方面有显著提升。社区活跃,已广泛应用于电商、广告、用户行为分析等领域。
Apache Doris 开源最顶级基于MPP架构的高性能实时分析数据库
|
14天前
|
缓存 关系型数据库 MySQL
高并发架构系列:数据库主从同步的 3 种方案
本文详解高并发场景下数据库主从同步的三种解决方案:数据主从同步、数据库半同步复制、数据库中间件同步和缓存记录写key同步,旨在帮助解决数据一致性问题。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
高并发架构系列:数据库主从同步的 3 种方案
|
9天前
|
关系型数据库 分布式数据库 数据库
PostgreSQL+Citus分布式数据库
PostgreSQL+Citus分布式数据库
40 15
|
6天前
|
存储 SQL API
探索后端开发:构建高效API与数据库交互
【10月更文挑战第36天】在数字化时代,后端开发是连接用户界面和数据存储的桥梁。本文深入探讨如何设计高效的API以及如何实现API与数据库之间的无缝交互,确保数据的一致性和高性能。我们将从基础概念出发,逐步深入到实战技巧,为读者提供一个清晰的后端开发路线图。
|
6天前
|
监控 API 微服务
后端技术演进:从单体架构到微服务的转变
随着互联网应用的快速增长和用户需求的不断演化,传统单体架构已难以满足现代软件开发的需求。本文深入探讨了后端技术在面对复杂系统挑战时的演进路径,重点分析了从单体架构向微服务架构转变的过程、原因及优势。通过对比分析,揭示了微服务架构如何提高系统的可扩展性、灵活性和维护效率,同时指出了实施微服务时面临的挑战和最佳实践。
25 7
|
7天前
|
存储 NoSQL 分布式数据库
微服务架构下的数据库设计与优化策略####
本文深入探讨了在微服务架构下,如何进行高效的数据库设计与优化,以确保系统的可扩展性、低延迟与高并发处理能力。不同于传统单一数据库模式,微服务架构要求更细粒度的服务划分,这对数据库设计提出了新的挑战。本文将从数据库分片、复制、事务管理及性能调优等方面阐述最佳实践,旨在为开发者提供一套系统性的解决方案框架。 ####
|
8天前
|
存储 SQL 数据库
深入浅出后端开发之数据库优化实战
【10月更文挑战第35天】在软件开发的世界里,数据库性能直接关系到应用的响应速度和用户体验。本文将带你了解如何通过合理的索引设计、查询优化以及恰当的数据存储策略来提升数据库性能。我们将一起探索这些技巧背后的原理,并通过实际案例感受优化带来的显著效果。
26 4
|
10天前
|
消息中间件 数据库 云计算
微服务架构下的数据库事务管理策略####
在微服务架构中,传统的单体应用被拆分为多个独立的服务单元,每个服务维护自己的数据库实例。这种设计提高了系统的可扩展性和灵活性,但同时也带来了分布式环境下事务管理的复杂性。本文探讨了微服务架构下数据库事务的挑战,并深入分析了几种主流的事务管理策略,包括Saga模式、两阶段提交(2PC)以及基于消息的最终一致性方案,旨在为开发者提供一套适应不同业务场景的事务处理框架。 ####
|
16天前
|
SQL Java 数据库连接
在Java应用中,数据库访问常成为性能瓶颈。连接池技术通过预建立并复用数据库连接,有效减少连接开销,提升访问效率
在Java应用中,数据库访问常成为性能瓶颈。连接池技术通过预建立并复用数据库连接,有效减少连接开销,提升访问效率。本文介绍了连接池的工作原理、优势及实现方法,并提供了HikariCP的示例代码。
30 3