2024年1月

创建一个WebApi项目,并且创建一个Dockerfile空文件,添加以下代码,7.0代表的你项目使用的SDK的版本,构建的时候也需要选择好指定的镜像tag

FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["WebApplication1/WebApplication1.csproj", "WebApplication1/"]
RUN dotnet restore "./WebApplication1/./WebApplication1.csproj"
COPY . .
WORKDIR "/src/WebApplication1"
RUN dotnet build "./WebApplication1.csproj" -c $BUILD_CONFIGURATION -o /app/build

FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./WebApplication1.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "WebApplication1.dll"]

我们一步一步来进行讲解,首先第一步,FROM指定了一个mcr.microsoft.com/dotnet/aspnet:7.0镜像,并且AS别名base,这一步是选择aspnet:7.0作为基础运行镜像,as别名用于方便以下步骤使用

FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

然后下一步,在这里FROM了一个mcr.microsoft.com/dotnet/sdk:7.0 的镜像并且AS别名build,这个镜像当中是使用到了.NET SDK作为镜像用于将项目构建成部署文件,在这里的COPY的目录的位置是从你docker build ./ 的这个./的目录作为根目录的,请注意如果出现文件未找到的情况下可能就是工作目录与Dockerfile目录不一致导致,在构建的时候指定了构建完成到/app/build目录中。

FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["WebApplication1/WebApplication1.csproj", "WebApplication1/"]
RUN dotnet restore "./WebApplication1/./WebApplication1.csproj"
COPY . .
WORKDIR "/src/WebApplication1"
RUN dotnet build "./WebApplication1.csproj" -c $BUILD_CONFIGURATION -o /app/build

在这里FROM了上面的build然后继续AS别名了publish在这里使用了dotnet publish将源码进行了构建部署,上面的build操作是为了将编译过程和构建镜像的过程分离。

FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./WebApplication1.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

这里使用到了最开始的base然后别名final,然后指定工作目录,使用COPY --from指定了上面的publish的构建镜像,然后将镜像构建的/app/publish中目录下面的所有的文件COPY到/app下,然后给镜像配置容器启动时运行的命令,则是我们的.NET Core项目启动命令,执行我们的项目。

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "WebApplication1.dll"]

结尾

来着token的分享
技术群:737776595

前段时间跟同事吹水聊天时,吐槽一个话题,就是公司项目都需要做多语言,前端后端都需要做。

而且是需要支持8国语言翻译,每次弄起来都特麻烦,每加一个Key就需要去翻译其他7个语言出来添加,每新增一个项目就需要弄一遍多语言资源,重复且枯燥费时的体力活。

项目想法

本项目主打一个一处配置多语言,多处使用的想法。助力项目方便快捷实现国际化(多语言)。
主要解决的问题:

  • 自动翻译多语言资源。
  • 通用资源无需重复添加(关联项目即可)
  • 新增地区时自动同步且翻译已存在的资源。
  • 友好的界面配置。
  • 修改资源时可实时同步到资源客户端(使用SignalR接入)
  • 多语言资源批量打包导出。

目前主要功能如下(目前还只是雏形,不算完善):

  • 可配关联项目,主项目关联其他项目即可获取其他项目的多语言资源,相同Key则优先取主项目中的资源(即覆盖)

  • 批量导入已存在项目多语言资源(目前只支持JSON格式)

  • 多语言配置导出(目前支持json,toml,messages.properties, xml格式)

  • 可配置自动翻译其他语言的资源(目前接入百度翻译和有道翻译API,冷门地区语言翻译暂不支持,翻译不准确时可手动修改)

  • API/SignalR接入

  • 界面管理

  • .NET SDK 接入 (其他语言待开发)

测试环境欢迎体验并给点意见或提提ISSUE,当然欢迎PR一起完善项目。

http://47.119.20.666666/Projects

http://47.119.20.666666/swagger/index.html

项目地址:
https://github.com/fanslead/LinguaNex

应用场景

通过API/SDK拉取多语言资源加载,可选WebSocket对接实现即时更新多语言资源。

  • API后端项目响应内容,如错误码对应的Message国际化多语言处理。
  • Web项目国际化多语言集成,可导出多语言文件编译,或对接API/SDK即时获取加载数据。
  • APP项目与Web基本一致。
  • 骚操作:实现一个短Key完成长文章多语言显示。

效果图

image

image
image
image

image

image

image

image

项目设计

项目整体的功能很简单。就是多语言资源的管理以及项目接入这几个点。

业务流程:
添加resource:

添加culture:

运行环境

  • .NET 8
  • Redis
  • RabbitMQ(可选)
  • EF Core SQLLite(可自行替换数据库)

OpenApi接入

请求地址:/api/OpenApi/Resources/{ProjectId}?cultureName=&all=

  • ProjectId表示项目ID
  • cultureName 可选参数,不传则默认当前请求环境语言资源。
  • all 可选参数,默认false,cultureName为空时,true则返回所有语言资源

响应结构如下:

[
  {
    "cultureName": "zh-Hans",
    "resources": {
      "Hello": "你好"
    }
  },
  {
    "cultureName": "en",
    "resources": {
      "Hello": "Hello"
    }
  }
]

SignalR接入(c#例子)

var connection = new HubConnectionBuilder()
    .WithUrl($"{linguaNexApiUrl}/hubs/LinguaNex?project={project}", Microsoft.AspNetCore.Http.Connections.HttpTransportType.WebSockets)
    .AddJsonProtocol()
    .WithAutomaticReconnect()
    .Build();

connection.On<LinguaNexResources>("CreateOrUpdateResource", obj => 
{
    if (_resourcesCache.TryGetValue(obj.CultureName, out var value))
    {
        foreach (var resource in obj.Resources)
        {
            value[resource.Key] = resource.Value;
        }
        _resourcesCache[obj.CultureName] = value;
    }else
    {
        _resourcesCache[obj.CultureName] = new ConcurrentDictionary<string, string>(obj.Resources);
    }
});

connection.StartAsync();

//拉取资源 参数跟OpenApi接口一致
connection.InvokeAsync<List<LinguaNexResources>>("GetResources", projectId, cultureName,all);

.NET SDK接入

需要引用LinguaNex.Localization.Net包。
在依赖注入中添加如下代码即可:

builder.Services.AddLinguaNexLocalization(options =>
{
    options.LinguaNexApiUrl = builder.Configuration["LinguaNex:ApiUrl"];
    options.Project = builder.Configuration["LinguaNex:Project"];
    options.UseWebSocket = true;
});

.NET SDK是通过实现IStringLocalizer接口对接的。所以可以无缝衔接已有多语言的项目切换。
本项目自身已使用SDK对接自己。
测试效果

antd design pro使用API接入

由于目前还没时间完事前端界面的多语言配置。但是之前试过使用antd design pro接入过ABP的后端多语言的配置。
接入流程是一致的。这里提供下参考代码:

import { addLocale, getLocale } from 'umi';
import { Locale } from 'antd/es/locale';

let locale = getLocale() as string;

let antdLocale = {} as Locale

const applicationLocalization = await getApplicationLocalization(); // 这里可以实现接入后端API获取资源
const resources = applicationLocalization.resources
const allText = {}

for(let key in resources) {
  if(resources[key]){
    Object.assign(allText, resources[key].texts)
  }
}
locale = getLocale() as string;
addLocale(
  locale,
  allText,
  {
    momentLocale: antdLocale.locale,
    antd: antdLocale,
  }

RoadMap






















按照固有的思维方式,如果想要语音克隆首先得有克隆对象具体的语言语音样本,换句话说,克隆对象必须说过某一种语言的话才行,但现在,coqui-ai TTS V2.0版本做到了,真正的跨语种无需训练的语音克隆技术。

coqui-ai TTS实现跨语种、无需训练克隆语音的方法是基于Tacotron模型,该模型使用了一种音素输入表示来鼓励在不同语种之间共享模型容量。此外,还引入了对抗损失项,以鼓励模型将说话者身份与语音内容进行解耦。这使得模型能够在不同语种之间进行语音合成,而无需在任何双语或平行示例上进行训练。

具体来说,coqui-ai TTS首先使用音素输入表示:采用音素(语音的基本发音单位)作为输入表示,鼓励模型在不同语种之间共享模型容量,从而实现跨语种语音合成。

随后引入对抗损失项:对抗损失项的引入有助于模型将说话者身份与语音内容进行解耦,从而使模型能够在不同语种之间进行语音合成,而无需在双语或平行示例上进行训练。

此外,通过在训练过程中使用多个讲话者的语音数据,并引入自动编码输入来帮助稳定注意力,进一步扩展了模型的规模,使其能够在所有训练过程中看到的语种中一致地合成可理解的语音,包括训练讲话者的本地口音或外国口音。

本次我们基于coqui-ai TTS的2.0版本来让钢铁侠托尼斯塔克先生开口讲16国语言。

coqui-ai TTS语音克隆项目配置

首先克隆项目:

git clone https://github.com/v3ucn/coqui-ai_xTTS_v2.2_webui_cn.git

注意该项目并非官方项目,而是在其基础上的修改版本,添加了中文版本的webui。

进入项目的目录:

cd coqui-ai_xTTS_v2.2_webui_cn

随后安装依赖:

pip install -r requirements.txt

安装完成后,先在Python终端里测试一下:

import torch  
from TTS.api import TTS

如果报下面这个错误:

from pydantic.typing import Annotated  
ImportError: cannot import name 'Annotated' from 'pydantic.typing'

那么说明pydantic库的版本过高了,进行降级即可:

pip install pydantic<2

coqui-ai TTS语音克隆模型配置

随后下载2.0版本的模型,下载地址:

https://huggingface.co/coqui/XTTS-v2/tree/main

将其放入项目的models目录,结构如下:

E:\work\coqui-ai_xTTS_v2.2_webui_cn\models\tts>tree /f  
Folder PATH listing for volume myssd  
Volume serial number is 7CE3-15AE  
E:.  
│  
├───tts_models--multilingual--multi-dataset--xtts_v2  
│       config.json  
│       dvae.pth  
│       hash.md5  
│       mel_stats.pth  
│       model.pth  
│       speakers_xtts.pth  
│       tos_agreed.txt  
│       vocab.json

随后,需要做一件重要的事,那就是配置模型目录的环境变量 TTS_HOME = E:\work\coqui-ai_xTTS_v2.2_webui_cn\models\

如图所示:

如果不设置环境变量,coqui-ai TTS会自动重复下载模型到C盘,非常的烦人。

coqui-ai TTS跨语种语音克隆推理

万事俱备,只欠推理,在终端执行命令:

python3 app.py

程序返回:

E:\work\coqui-ai_xTTS_v2.2_webui_cn>python app.py  
 > tts_models/multilingual/multi-dataset/xtts_v2 is already downloaded.  
 > Using model: xtts  
Running on local URL:  http://127.0.0.1:7860  
  
To create a public link, set `share=True` in `launch()`.

注意程序初始化比较慢,另外设置了环境变量系统就会侦测到模型已下载。

此时访问
http://127.0.0.1:7860

默认用钢铁侠英文30秒素材作为克隆的数据集。

选择语速、语言即可直接推理,方便快捷。

这里需要注意的是,如果想让钢铁侠的音色讲日语,那么需要单独安装Mecab库,并且单独拷贝动态库,详见:
Win11环境Mecab日语分词和词性分析以及动态库DLL not found问题(Python3.10)
,这里不再赘述。

除了钢铁侠的音色,我们也可以自主添加别的角色音色:

比如添加生化危机角色艾达王,那么把艾达王30秒的语音wav文件放入到项目的targets目录即可,命名规范:角色名.wav。

随后就可以在webui界面中选择艾达王的音色进行克隆。

结语

coqui-ai xtts支持多种语言,包括汉语、英语、韩语、日语、西班牙语、法语等。这意味着您可以使用coqui-ai xtts来合成多种语言的语音,而无需进行额外的训练或调整。其基于先进的深度学习技术,能够生成高质量、自然流畅的语音。这意味着即使在不同语种之间,coqui-ai xtts生成的语音也能保持高质量和自然度,正是居家旅行,口播嘴替的必备好库。

前言

MySQL和Redis数据一致性算是个很经典的问题,在之前也看到过很多相关的文章,最近心血来潮,想把一致性问题的解决方案和存在问题都总结一下。

不推荐方案

1 先更新MySQL,再更新Redis。

image-20240103204207546

如上图有两个请求要同时进行更新操作,在并发情况下,B请求虽然更新时间晚于A请求,但是可能因为网络延迟问题,导致本来A请求要先更新Redis的操作晚于B请求更新Redis的操作,最终导致了MySQL出现数据不一致。

2 先更新Redis,在更新MySQL。

image-20240103204224684

这种情况其实等同于第一种情况。

3 先删除Redis缓存,再更新MySQL。

image-20240103204235856

A请求对数据的更新操作晚于请求B的读取操作,导致B请求将数据库的旧值又写回缓存,删除缓存在这种情况下没有意义。

推荐方案

1 先删除Redis缓存,再更新MySQL,再删一次Redis缓存(延迟双删)

image-20240103205637142

在第三种情况中,出现了删除缓存后被其他请求更新为旧值的情况,那么在这种情况下,再删除一遍缓存不就可以解决问题了。这里第二次删除缓存的时间必须在B请求回写旧值之后,所以要社招好第二次删除缓存的等待时间,根据业务实际耗时来定,假设B请求回写缓存要300ms,那么A请求可以设置等待500ms再进行缓存删除。

但是上面这种情况也会出现问题,比如延迟双删的时候删除缓存失败怎么办。

这个时候可以借助MQ重试机制。如下图:

image-20240104092831624

将删除的请求放到MQ队列里面,然后系统再从MQ里面取出删除请求的操作,由于MQ支持失败重试,删除失败后会继续投递消息。

2 先更新MySQL,再删除Redis缓存。

image-20240104094222584

在上面这种情况下,请求B出现了读取了一次旧值,如果对于业务是一致性要求没那么强的话(比如秒杀,减库存),这种方案也是可以的,误差范围是可以接收的,只存在这么一次数值是旧的情况。

当然还有特殊情况如下:

image-20240104095635485

当B请求先查询Redis,这个时候redis刚好缓存失效,B请求就会去MySQL查询旧值,后续B请求回写旧值的请求又晚于A请求删除缓存的请求,导致缓存里面放的是旧值。

但是这种情况出现需要 同时满足以下两个条件:

(1)缓存刚好失效

(2)读请求回写缓存的时间晚于写请求回写缓存的时间

上述两个条件同时成立的概率是极小的,综上来说,这种方案还是不错的,复杂度也不高,但同时也是可能存在删除缓存失败的特殊情况导致误差。

3 先更新MySQL,通过 Binlog,异步更新 Redis

image-20240104101137666666

A请求更新完MySQL,借助Canal进行监听并把相关的修改记录推送到MQ,MQ经过消费系统拉取消息对Redis进行更新,如果在Redis更新之前,有新的读请求,依然会导致数据不一致性的问题,但是这种方案能够实现最终一致性。

在这里Canal作为一个组件,监听binlog和发送消息到MQ都由Canal完成。

方案总结

前三种方案都是不推荐使用的。对于推荐使用的方案,从实时性和技术复杂度来说,先写数据库再删除缓存是比较好的选择。如果要确保最终一致性的话,可以用binlog异步更新缓存的方案。

作者:vivo 互联网服务器团队 - Zhou Shaobin

本文主要介绍了Spring事务传播性的相关知识。

Spring中定义了7种事务传播性:

  • PROPAGATION_REQUIRED

  • PROPAGATION_SUPPORTS

  • PROPAGATION_MANDATORY

  • PROPAGATION_REQUIRES_NEW

  • PROPAGATION_NOT_SUPPORTED

  • PROPAGATION_NEVER

  • PROPAGATION_NESTED

在Spring环境中,含有事务的方法嵌套调用,事务是如何传递的规则,以及每种规则是如何开展工作的。文章还提到每种事务传播性是如何使用的,方便读者依据实际的场景,使用不同的事务规则。

一、什么是Spring事务的传播性

Spring 事务传播性是指, 在Spring的环境中,当多个含有事务的方法嵌套调用时,每个事务方法都处于自己事务的上下文中,其提交或者回滚行为应该如何处理。

通俗讲,就是当一个事务方法调用另外一个事务方法时,事务如何跨上下文传播。

图片

1)当事务方法A调用事务方法B时,事务方法B是合并到事务方法A中,还是开启新事务?

2)当事务方法B抛出异常时  ,在合并事务或者开启新的事务的场景中,事务的回滚是如何处理的 ?

以上事务的处理规则,都取决于事务传播级别的设置。

二、事务的传播性都有哪些行为

图片

事务的传播行为,主要分为三种类型,分别是:
支持当前事务

不支持当前事务

嵌套事务

2.1 支持当前事务

REQUIRED
:默认的事务传播级别,表示如果当前方法已在事务内,该方法就在当前事务中执行,否则,开启一个新的事务并在其上下文中执行。

SUPPORTED
:当前方法在事务内,则在其上下文中执行该方法,否则,开启一个新的事务。

MANDATORY
:必须在事务中执行,否则,将抛出异常。

2.2 不支持当前事务

REQUIRES_NEW
:无论当前是否有事务上下文,都会开启一个事务  。如果已经有一个事务在执行 ,则正在执行的事务将被挂起 ,新开启的事务会被执行。

事务之间相互独立,互不干扰。

NOT_SUPPORTED
:不支持事务,如果当前存在事务上下文,则挂起当前事务,然后以非事务的方式执行。

NEVER
:不能在事务中执行,如果当前存在事务上下文,则抛出异常。

2.3 嵌套事务

NESTED
:嵌套事务,如果当前已存在一个事务的上下文中,则在嵌套事务中执行,如果抛异常,则回滚嵌套事务,而不影响其他事务的操作。

三、每种事务的传播性如何工作

3.1 REQUIRED

默认的事务传播行为,保证多个嵌套的事务方法在同一个事务内执行,并且同时提交,或者出现异常时,同时回滚。

这个机制可以满足大多数业务场景。

图片

例子 :

图片

图片

1)类TestAService的方法通过声明式事务的方式,加上了事务注解@Transactional ,并设置事务的传播性为REQUIRED。

2)调用者调用TestAService的A方法时,如果调用者没有开启事务,那么A方法会开启一个事务。

A方法的具体执行过程如下 :

a. 执行insert,但没有提交;

b. 调用TestBServcie的B方法,由于B方法也声明了事务,并且传播性是REQUIRED,所以方法B的事务,合并到方法A开启的事务中。

c. 方法B执行insert操作,此时也没有提交。

3)由于这两个方法的操作都在同一个事务中执行,当这两个方法所有操作执行成功之后,提交事务。

嵌套调用链路:

图片

当方法B 执行时抛出了 Exception 异常后,事务是如何处理的 ?

1)方法B声明了事务,insert操作会回滚

2)由于方法A和方法B 同属一个事务,方法A也会执行回滚,由此说明该规则保证了事务的原子性。

嵌套调用,异常后的链路:

图片

如果 方法B 抛出异常后,方法A 使用 try-catch 处理了方法B的异常(如下代码),并没有向外抛出,此时事务又如何处理的 ?

图片

方法A也会回滚。

从事务的特性我们可知,事务具有原子性。方法A和方法B同属一个事务,当方法B抛出异常,触发回滚操作后,整个事务的操作都会回滚。

因此,Spring 在处理事务过程中,当事务的传播性设置为REQUIRED,在整个事务的调用链上,任何一个环节抛出的异常都会导致全局回滚。

3.2 REQUIRES_ NEW

每次都开启一 个新的事务。

图片

例子:

图片

上面例子中,方法B的传播性设置为 REQUIRES_NEW,方法A仍然是REQUIRED,当A调用B时,具体调用链路如下:

图片

具体执行过程:

  • 方法A被执行前,如果调用者没有开启事务,方法A开启一个事务1,然后执行insert ,此时没有提交;

  • 方法B的事务传播性设置为REQUIRES_NEW,当被方法A调用时,此时方法A的事务1会被挂起,方法B开启自己的事务2,然后执行insert,此时并没有提交;

  • 当方法B执行完毕后,提交事务2;

  • 恢复事务1,最终提交。

当 方法B 执行时抛出了异常,会发生什么?

方法B的insert操作会被回滚掉,方法A不受影响。但这里有个前提,方法A需要try-catch方法B的异常,使其异常不会往上传递,从而导致方法A接收到异常,导致回滚。

图片

3.3  SUPPORTED

当外层方法A存在事务,方法B加入到当前事务中,以事务的方式执行。

图片

当外层方法A不存在事务,方法B不会创建新的事务,以非事务的方式执行。

图片

例子1:

图片

图片

以上例子,方法A没有加事务注解,方法B的加了事务注解,并且传播为SUPPORTS。

具体执行过程:

  • 方法A以非事务的方式执行insert操作。

  • 方法B被调用,由于其外层事务A没有开启事务,方法B也是以非事务方法执行insert操作。

图片

例子2:

图片

以上例子,方法A和B都加上了事务注解,其中方法A的传播性为REQUIRED,方法B的传播性为SUPPORTS。

具体执行过程:

  • 如果方法A的调用方没有开启事务,则方法A开启事务,并执行insert操作,但没有提交;

  • 方法B被调用,由于其外层方法A开启了事务,因此方法B加入到方法A开启的事务中,并执行insert,但没有提交;

  • 当事务中的所有操作执行成功后,事务提交。

图片

3.4  NOT_SUPPORTED

不支持事务。

如果外层方法存在事务,则挂起外层事务,以非事务方式执行,执行完毕后,恢复外层事务。

图片

例子:

图片

以上例子:方法A和B都加上了事务注解,方法A的传播性为REQUIRED,方法B为NOT_SUPPORTED。

具体执行过程:

  • 如A的调用方没有开启事务,方法A开启事务,并执行insert,但没有提交。

  • 方法A调用方法B时,方法B的传播性为NOT_SUPPORTED,不支持事务,然后挂起外层方法A的事务,方法B以非事务的方式执行insert。

  • 方法B执行完毕后,恢复方法A的事务,最终提交事务。

调用链路过程:

图片

3.5 NEVER

不支持事务

当外层方法A开启了事务,方法B抛出异常

图片

例子:

图片

以上代码,两个方法都打上了事务注解,方法A的传播性是REQUIRED,方法B的传播性是NEVER。

具体执行过程:

  • 方法A开启事务,执行insert,没有提交。

  • 含有事务的方法A调用方法B,方法B的传播性是NEVER,表示不支持事务,因此方法B抛出异常。

  • 方法A的事务执行回滚。

图片

3.6 MANDATORY

必须在事务中执行。

如果外层方法A没有开启事务,方法B抛出异常。

图片

如果外层方法A开启了事务,方法B加入事务,方法A&B在同一事务中执行。

图片

例子:

图片

以上例子,方法A没有加事务注解,方法B 的传播性为 MANDATORY。

具体执行过程:

  • 方法A的调用方如果本身没有开启事务,方法A执行前不会开启事务。

  • 当非事务方法A调用方法B时,由于方法B的传播性为MANDATORY,必须在事务中执行,条件不满足,抛出异常。

图片

3.7 NESTED

嵌套事务

  • 如果外层方法A不存在事务,内层方法B的规则与REQUIRED 一致。

  • 如果外层方法A存在事务,内层方法B做为外层方法A事务的子事务执行,两个方法是一起提交,但子事务是独立回滚。

    内层方法B抛出异常,则会回滚方法B的所有操作,但不影响外层事务方法A。(方法A需要try-catch子事务,避免异常传递到父层事务)

    外层方法A回滚,则内层方法B也会回滚。

  • 该传播性的特点是可以保存状态点,当回滚时,只会回滚到某一个状态点,保证了子事务之间的独立性,避免嵌套事务的全局回滚。

图片

例子:

图片

以上例子,方法A的传播性为REQUIRED,方法B为NESTED。

具体执行过程:

  • 方法A执行时,如调用方没有开启事务,则开启一个事务。

  • 方法B被外层方法A调用时,因为方法B的传播性为NESTED,方法B在此处建立savepoint,标记insert行为。

  • 当方法B抛出异常,其insert操作会回滚,但只会回滚到savepoint,(前提是方法A要try-catch方法B,使方法B的异常不会往外传递)。

  • 方法B回滚后,方法A的事务提交。

调用链路:

图片

四、总结

本文解释了Spring框架中的事务传播性,即多个业务方法之间调用时事务如何处理的规则。Spring提供了七种传播级别,如PROPAGATION_REQUIRED、PROPAGATION_REQUIRES_NEW等。

每种级别都有适用场景和限制,本文提供了一些示例,介绍了声明式事务如何使用,每种事务的规则,产生哪种行为,当方法抛出异常时,事务的提交和回滚是如何被处理的。正确处理事务对于任何企业级应用程序都是必要的,了解Spring事务传播性是构建高效、可靠和可扩展应用程序的关键。