Java Spring 事物处理

一、定义

事务(Transaction)是指作为单个逻辑工作单元执行的一系列操作。操作要么全部成功执行,要么全部失败回滚,以确保数据的一致性和完整性。

二、特性

  1. 原子性(Atomicity):事务被视为不可分割的工作单元,要么全部执行成功,要么全部失败回滚。即使在执行过程中发生了错误,事务也会被回滚到最初的状态,不会留下部分执行的结果。

  2. 一致性(Consistency):事务在执行前后,数据库中的数据应该保持一致性。这意味着事务的执行不会破坏数据库的完整性约束,如唯一性约束、外键约束等。

  3. 隔离性(Isolation):多个事务并发执行时,每个事务的操作应该相互隔离,互不干扰。即使多个事务同时对同一数据进行操作,也不应该相互影响,以避免数据的不一致性。

  4. 持久性(Durability):一旦事务执行成功提交,其结果应该对其他事务可见,并且应该持久保存在系统中,即使系统发生故障或重启,事务的提交结果也不会丢失。

在编程中,特别是在数据库操作中,事务通常用于确保对数据库的一系列操作是原子性的,要么全部成功提交,要么全部回滚。这有助于确保数据的完整性和一致性。

三、事物处理方式

Spring 框架提供了多种方式来处理事务,包括声明式事务管理和编程式事务管理。

  • 声明式事务管理:Spring 声明式事务管理在底层采用了 AOP 技术,其最大的优点在于无须通过编程的方式管理事务,只需要在配置文件中进行相关的规则声明,就可以将事务规则应用到业务逻辑中。
    其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。

  • 编程式事务管理:除了声明式事务管理外,Spring 框架还提供了编程式事务管理的方式。通过使用 TransactionTemplatePlatformTransactionManager 接口,开发人员可以在代码中显式地控制事务的开始、提交和回滚。

四、声明式事务管理注解方式

1. 启用 Spring 的注释驱动事务管理功能

@EnableTransactionManagement 是 Spring 框架提供的一个注解,用于启用注解驱动的事务管理。一旦 @EnableTransactionManagement 注解被添加,Spring 容器将会自动扫描带有 @Transactional 注解的方法,并为其创建代理以支持事务管理。

在 Spring 应用程序中,@EnableTransactionManagement 注解可以被用于配置类(通常是一个带有 @Configuration 注解的类)和启动类(通常是带有 @SpringBootApplication 注解的类)。虽然在这两种情况下都能够启用注解驱动的事务管理,但它们之间存在一些细微的区别。

配置类上使用 @EnableTransactionManagement

  • @EnableTransactionManagement 注解被添加到一个专门的配置类上时,通常用于集中配置和管理事务相关的设置,包括数据源、事务管理器等。

  • 配置类可以包含其他与应用程序配置相关的 bean 定义,例如数据源、JPA 实体管理工厂、持久化单元等。

  • 这种方式使得整个应用程序的事务管理配置更加集中和清晰,同时也使得代码更易于维护和理解。

启动类上使用 @EnableTransactionManagement

  • @EnableTransactionManagement 注解被添加到 Spring Boot 的启动类上时,通常用于启用整个应用程序范围内的注解驱动的事务管理。

  • 启动类是 Spring Boot 应用程序的入口点,通过在启动类上添加 @EnableTransactionManagement 注解,可以方便地启用注解驱动的事务管理,并为整个应用程序启用事务支持。

  • 这种方式使得整个应用程序范围内的事务管理配置更加简单,适用于小型应用程序或需要全局事务支持的情况。

总的来说,无论是在配置类上使用 @EnableTransactionManagement 还是在启动类上使用 @EnableTransactionManagement,都可以有效地启用注解驱动的事务管理。选择使用哪种方式取决于应用程序的规模、结构和需求。如果需要更加灵活的事务管理配置,通常会选择在配置类上使用 @EnableTransactionManagement;而如果只需要简单地启用全局的事务支持,可以选择在启动类上使用 @EnableTransactionManagement。

2. 定义事务管理器

事务交给 spring 管理,那么你肯定要创建一个或者多个事务管理者,有这些管理者来管理具体的事务
在 Spring 应用程序中,要定义事务管理器,通常可以通过创建一个 bean 来配置事务管理器。事务管理器负责协调和管理数据库事务的提交和回滚。在 Spring 中,可以使用 PlatformTransactionManager 接口的实现来定义事务管理器,具体取决于应用程序使用的数据访问技术,比如 JDBC、JPA 或 Hibernate 等。

PlatformTransactionManager 多个实现类,用来应对不同的环境

  • JpaTransactionManager: 如果你用 jpa 来操作 db,那么需要用这个管理器来帮你控制事务。

  • DataSourceTransactionManager: 如果你用是指定数据源的方式,比如操作数据库用的是:JdbcTemplate、mybatis、ibatis,那么需要用这个管理器来帮你控制事务。

  • HibernateTransactionManager: 如果你用 hibernate 来操作 db,那么需要用这个管理器来帮你控制事务。

  • JtaTransactionManager: 如果你用的是 java 中的 jta 来操作 db,这种通常是分布式事务,此时需要用这种管理器来控制事务。

以下是一个简单的示例:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;import javax.sql.DataSource;@Configuration
public class TransactionConfig {@Beanpublic PlatformTransactionManager transactionManager(DataSource dataSource) {return new DataSourceTransactionManager(dataSource);}
}

3. 使用 @Transaction

在 Spring 框架中,@Transactional 注解用于声明一个方法是事务性的。通过在方法上添加 @Transactional 注解,开发人员可以指定该方法应该在一个事务中执行。这意味着,在方法执行期间,Spring 框架会自动管理事务的开始、提交、回滚等操作,以确保数据的一致性和完整性。

  • 方法级别使用:最常见的用法,放在 public 方法上,那么该方法将被 spring 自动加上事务。
  • 放在类上,那么当前类以及其下无限级子类中所有 pubilc 方法将被 spring 自动加上事务。
  • 放在接口上,那么接口的实现类中所有 public 都被 spring 自动加上事务。

@Transaction 参数说明

  1. value:指定事务管理器的 bean 名称,如果容器中有多事务管理器 PlatformTransactionManager,那么你得告诉 spring,当前配置需要使用哪个事务管理器

  2. transactionManager:同 value,value 和 transactionManager 选配一个就行,也可以为空,如果为空,默认会从容器中按照类型查找一个事务管理器 bean

  3. propagation:指定事务的传播行为。事务的传播行为定义了在不同事务边界内执行方法时,方法如何参与现有事务或者创建新的事务。可以设置为下列之一:

    • Propagation.REQUIRED:如果当前存在事务,则加入该事务;如果不存在,则创建一个新的事务。
      这是默认的传播行为,适用于大多数情况。它确保方法总是在一个事务中执行。

    • Propagation.REQUIRES_NEW:始终创建一个新的事务。
      使用这种传播行为,方法总是在一个新的事务中执行。即使调用方法的代码已经处于一个事务中,该方法也会挂起当前事务,并创建一个新的事务进行执行。

    • Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果不存在事务,则以非事务的方式执行。
      这种传播行为表示方法可以在事务中执行,也可以在非事务的环境中执行。如果当前存在事务,则方法将加入该事务;如果没有事务,则方法以非事务的方式执行。

    • Propagation.NOT_SUPPORTED:以非事务的方式执行。如果当前存在事务,则将其挂起。
      使用这种传播行为,方法总是以非事务的方式执行。即使调用方法的代码已经处于一个事务中,该方法也会挂起当前事务,并以非事务的方式执行。

    • Propagation.MANDATORY:当前方法必须在一个事务中执行,否则会抛出异常。
      这种传播行为要求调用方法的代码必须在一个事务中执行,如果没有事务存在,则会抛出异常。

    • Propagation.NEVER:以非事务的方式执行。如果当前存在事务,则抛出异常。
      使用这种传播行为,方法总是以非事务的方式执行。如果调用方法的代码已经处于一个事务中,则会抛出异常。

    • Propagation.NESTED:如果当前存在事务,则在嵌套事务中执行;如果不存在事务,则创建一个新的事务。
      这种传播行为类似于 REQUIRED,但是可以在一个已存在的事务中创建一个嵌套事务。如果当前没有事务,则会创建一个新的事务。

  4. isolation:指定事务的隔离级别。事务隔离级别定义了在并发环境中,一个事务对数据的读取能力以及对其他事务修改的可见性。可以设置为下列之一:

    • Isolation.DEFAULT:使用数据库默认的隔离级别。通常情况下,数据库会使用一种默认的隔禅级别,这取决于具体的数据库系统。

    • Isolation.READ_UNCOMMITTED:一个事务可以读取另一个事务未提交的数据修改。这种隔离级别最低,不保证事务的一致性和隔离性。允许脏读、不可重复读和幻影读。

    • Isolation.READ_COMMITTED:一个事务只能读取另一个事务已经提交的数据修改。这种隔离级别可以避免脏读,但是仍然可能出现不可重复读和幻影读。

    • Isolation.REPEATABLE_READ:一个事务在多次读取同一数据时,能够保证读取到的数据是一致的。这种隔离级别可以避免脏读和不可重复读,但是仍然可能出现幻影读。

    • Isolation.SERIALIZABLE:最高的隔离级别,确保事务可以完全地隔离其他事务的并发操作,避免脏读、不可重复读和幻影读。这种隔离级别对数据库性能有较大的影响,因为可能需要对数据进行严格的锁定。

说明

  • 脏读(Dirty Read):脏读指的是一个事务读取了另一个事务未提交的数据。当一个事务对数据进行了修改但还未提交时,另一个事务读取到了这些未提交的数据,这就是脏读。如果第一个事务最终回滚了,那么另一个事务读取到的数据就是无效的。
    脏读会导致事务读取到不一致甚至无效的数据,破坏了事务的隔离性和一致性。

  • 不可重复读(Non-Repeatable Read):不可重复读指的是在一个事务内,多次读取同一数据时,得到了不同的结果。这是因为在事务执行期间,另一个事务对数据进行了修改并提交,导致第一个事务多次读取同一数据时得到了不同的结果。
    不可重复读会导致事务在同一数据上得到不一致的结果,也破坏了事务的隔离性和一致性。

  • 幻影读(Phantom Read):幻影读指的是在一个事务内,多次执行同一个查询,但得到了不同数量的数据。这是因为在事务执行期间,另一个事务对数据进行了插入或删除,导致第一个事务多次执行相同的查询时得到了不同数量的数据。

  • 幻影读会导致事务在相同的查询条件下得到不同数量的数据,同样破坏了事务的隔离性和一致性。

  1. timeout:指定事务的超时时间(以秒为单位)。如果事务在指定的时间内未能完成,则会被回滚。

  2. readOnly:指定事务是否为只读事务。如果设置为 true,则表示该事务只读取数据,不进行写操作,可以优化数据库性能。
    如果在一个标记为只读事务的方法中进行了数据更新、插入或删除操作,可能会导致事务提交失败或抛出异常。因此,在使用 readOnly 参数时,需要确保方法的实际操作与事务的只读属性相符。

  3. rollbackFor:指定哪些异常会触发事务回滚。可以设置为一个异常类型的数组,当发生这些异常之一时,事务会回滚。

  4. noRollbackFor:指定哪些异常不会触发事务回滚。同样可以设置为一个异常类型的数组。

  5. rollbackForClassName:与 rollbackFor 类似,但是接受异常类名的字符串数组,而不是实际的异常类。

  6. noRollbackForClassName:与 noRollbackFor 类似,但是接受异常类名的字符串数组,而不是实际的异常类。

这些参数可以在 @Transactional 注解中根据需要进行组合和配置,以便满足特定的事务管理需求。通过灵活地使用这些参数,可以精细地控制事务的行为和特性,以确保数据操作的一致性和完整性。

五、编程式事务管理方式

PlatformTransactionManager

在 Spring 框架中,PlatformTransactionManager 是用于控制事务的关键接口之一。它定义了一系列方法,用于管理事务的生命周期、提交事务以及回滚事务等操作。通过 PlatformTransactionManager 接口,我们可以在 Spring 应用中对事务进行精细化的控制。

以下是使用 PlatformTransactionManager 控制事务的一般步骤:

  1. 获取 PlatformTransactionManager 的实例。

  2. 利用获取的实例创建 TransactionDefinition 对象,该对象定义了事务的传播行为、隔禂级别、超时时间等属性。

  3. 利用 PlatformTransactionManager 开启事务,并获取 TransactionStatus 对象,该对象表示了当前事务的状态。

  4. 在事务内部执行需要进行事务管理的操作。

  5. 根据执行结果决定提交事务或者回滚事务。

下面是一个简单的示例:

import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.PlatformTransactionManager;public class ProgrammaticTransactionExample {private PlatformTransactionManager transactionManager;public void setTransactionManager(PlatformTransactionManager transactionManager) {this.transactionManager = transactionManager;}public void performProgrammaticTransaction() {// 定义事务的传播行为、隔禂级别等属性DefaultTransactionDefinition def = new DefaultTransactionDefinition();def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);// 开启事务TransactionStatus status = transactionManager.getTransaction(def);try {// 在事务内部执行数据库操作// 如果发生异常,可以通过 status.setRollbackOnly() 进行事务回滚// 操作成功完成后,提交事务transactionManager.commit(status);} catch (Exception ex) {// 操作出现异常,回滚事务transactionManager.rollback(status);}}
}

TransactionTemplate

在 Spring 框架中,可以使用 TransactionTemplate 来实现编程式事务管理。TransactionTemplate 简化了编程式事务的操作,它封装了事务的开始、提交和回滚过程,使得在代码中控制事务变得更加方便。

以下是使用 TransactionTemplate 控制事务的一般步骤:

  1. 获取 PlatformTransactionManager 的实例。

  2. 创建 TransactionTemplate 实例,并将 PlatformTransactionManager 传入其构造函数。

  3. 通过 TransactionTemplate 提供的方法执行业务操作。

  • <T> T execute(TransactionCallback<T> action):有返回值,需要传递一个 TransactionCallback 对象,该对象定义了要在事务中执行的操作。
    TransactionCallback<T> 是一个函数式接口,执行时需要一个 TransactionStatus 类型的参数,以及返回个 T 类型的对象。
  • void executeWithoutResult(Consumer<TransactionStatus> action):没有返回值的,需传递一个 Consumer 对象,该对象定义了要在事务中执行的操作。
    Consumer<TransactionStatus> 是一个函数式接口,执行时需要一个 TransactionStatus 类型的参数,无返回结果。

源码中看的话 executeWithoutResult 方法是 TransactionOperations 接口中的一个默认方法 TransactionTemplate 实现了 TransactionOperations 接口,而在 TransactionOperations 接口中 executeWithoutResult 方法内其实是调用了 execute 方法执行的,只不过没有把 execute 方法的执行结果返回。
在使用这两种方法时,Spring 会自动管理事务的开始、提交和回滚过程,使得事务管理变得更加方便和简单。这样,您可以专注于业务逻辑的实现,而无需手动管理事务的细节。

下面是一个简单的示例:

import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;public class ProgrammaticTransactionWithTemplateExample {private PlatformTransactionManager transactionManager;private TransactionTemplate transactionTemplate;public void setTransactionManager(PlatformTransactionManager transactionManager) {this.transactionManager = transactionManager;this.transactionTemplate = new TransactionTemplate(transactionManager);}public String execute() {return transactionTemplate.execute(status -> {// 在事务内部执行数据库操作// 如果发生异常,可以通过 status.setRollbackOnly() 进行事务回滚return "操作成功"; // 操作成功完成后返回});}public void executeWithoutResult() {transactionTemplate.executeWithoutResult(status -> {// 在事务内部执行数据库操作// 如果发生异常,可以通过 status.setRollbackOnly() 进行事务回滚});}}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/44874.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

flutter Navigator跳转报错

Navigator operation requested with a context that does not include a Navigator. The context used to push or pop routes from the Navigator must be that of a widget that is a descendant of a Navigator widget. 这个报错是&#xff1a;因为你尝试使用 Navigator 操…

游戏AI的创造思路-技术基础-决策树(2)

上一篇写了决策树的基础概念和一些简单例子&#xff0c;本篇将着重在实际案例上进行说明 目录 8. 决策树应用的实际例子 8.1. 方法和过程 8.1.1. 定义行为 8.1.2. 确定属性 8.1.3. 构建决策树 8.1.4. 实施行为 8.1.5. 实时更新 8.2. Python代码 8. 决策树应用的实际例子…

滑动窗口,最长子序列最好的选择 -> O(N)

最近在学校上短学期课程&#xff0c;做程序设计题&#xff0c;一下子回忆起了大一学数据结构与算法的日子&#xff01; 这十天我会记录一些做题的心得&#xff0c;今天带来的是对于最长子序列长度题型的解题框架&#xff1a;滑动窗口 本质就是双指针算法&#xff1a; 通过le…

Vue路由传参和接参如何实现

在Vue中&#xff0c;使用Vue Router进行页面路由跳转时&#xff0c;经常需要传递参数到目标页面&#xff08;组件&#xff09;并在目标页面&#xff08;组件&#xff09;中接收这些参数。Vue Router提供了几种方式来实现路由传参和接参&#xff0c;主要包括通过URL的查询参数&a…

模拟生成高斯随机数序列

模拟和生成高斯随机数序列&#xff08;服从标准正态分布的随机变量&#xff09; Box-Muller 法 & Marsaglia 极坐标法 Box-Muller&#xff1a;使两个独立的均匀分布生成一个高斯分布。 Box-Muller方法的基本思想是利用两个独立的均匀分布随机变量的关系来生成高斯分布的…

Elasticsearch 多索引/多类型搜索

Elasticsearch&#xff0c;简称ES&#xff0c;是一个建立在Apache Lucene基础上的开源搜索引擎&#xff0c;它支持近乎实时的数据存储和检索&#xff0c;并具有良好的扩展性&#xff0c;可以处理PB级别的数据。在复杂的应用场景中&#xff0c;经常需要跨多个索引或类型进行搜索…

AcWing 1633:外观数列

【题目来源】https://www.acwing.com/problem/content/1635/【题目描述】 外观数列是指具有以下特点的整数序列&#xff1a;D, D1, D111, D113, D11231, D112213111, ...其中 D 是一个 [0,9] 范围内的不等于 1 的整数。 序列的第 n1 项是对第 n 项的描述。比如&#xff1a; 第 …

编程语言成长经历:探索、挑战与蜕变

编程语言成长经历&#xff1a;探索、挑战与蜕变 在数字化时代&#xff0c;编程语言无疑成为了连接人与机器的重要桥梁。回首我的编程语言成长经历&#xff0c;仿佛是一段充满探索、挑战与蜕变的旅程。 四个方面&#xff1a;初识编程的迷茫与好奇 当我第一次接触编程语言时&a…

Ubuntu与Windows通过WIFI与以太网口共享网络,Ubuntu与Windows相互ping通,但ping百度失败

Linux开发板&#xff08;正点原子阿尔法_IMX6U&#xff09;与Ubuntu的文件传输SCP 报错 SSH: no matching host key type found. Their offer: ssh-rsa-CSDN博客 前面的文章提到了如何将Ubuntu与Windows通过WIFI共享网络给以太网&#xff0c;从而实现Linux开发板、Ubuntu、Win…

香港优才计划续签难吗?一次性说清楚优才续签要求,不在香港居住也能续签成功!

香港优才计划续签难吗&#xff1f;这个问题对考虑申请优才的人来说&#xff0c;还是挺重要的。我们申请优才&#xff0c;最关注的2个问题&#xff0c;一个是获批&#xff0c;还有一个就是续签了。 毕竟我们费那么大功夫申请优才&#xff0c;可不只是为了一个为期3年的香港临时…

数据结构第20节 快速排序以及优化

快速排序是一种非常高效的排序算法&#xff0c;由英国计算机科学家托尼霍尔&#xff08;Tony Hoare&#xff09;在1960年代发明。它使用分治法&#xff08;Divide and Conquer&#xff09;策略来把一个序列分为较小的部分&#xff0c;然后递归地排序这些部分。 快速排序的基本…

Python 实现Word文档中提取表格数据并转换为CSV和JSON格式

python实现Word文档中提取表格数据 前言1.解析Word文档中的表格2.保存表格数据3.处理文件夹中的多个Word文档4.总结 前言 在日常工作中&#xff0c;我们经常需要处理大量的Word文档&#xff0c;其中包含各种表格数据。手动整理这些表格不仅耗时且容易出错。因此&#xff0c;开…

如何分析软件测试中发现的Bug!

假如你是一名软件测试工程师&#xff0c;每天面对的就是那些“刁钻”的Bug&#xff0c;它们像是隐藏在黑暗中的敌人&#xff0c;时不时跳出来给你一个“惊喜”。那么&#xff0c;如何才能有效地分析和处理这些Bug&#xff0c;让你的测试工作变得高效且有趣呢&#xff1f;今天我…

MongoDB - 集合和文档的增删改查操作

文章目录 1. MongoDB 运行命令2. MongoDB CRUD操作1. 新增文档1. 新增单个文档 insertOne2. 批量新增文档 insertMany 2. 查询文档1. 查询所有文档2. 指定相等条件3. 使用查询操作符指定条件4. 指定逻辑操作符 (AND / OR) 3. 更新文档1. 更新操作符语法2. 更新单个文档 updateO…

【ElasticSearch】ES 5.6.15 向量插件支持

参考 : https://github.com/lior-k/fast-elasticsearch-vector-scoring 下载插件 安装插件 插件目录&#xff1a; elasticsearch/plugins, 安装后的目录如下 plugins└── vector├── elasticsearch-binary-vector-scoring-5.6.9.jar└── plugin-descriptor.properties修…

linux中如何开启多个mysql进城并且单独设置密码和端口

在 Linux 系统中&#xff0c;默认情况下 MySQL&#xff08;或 MariaDB&#xff0c;一个流行的 MySQL 分支&#xff09;不直接支持在同一台机器上运行多个 MySQL 实例&#xff0c;每个实例使用不同的端口和独立的密码系统。但是&#xff0c;你可以通过配置多个 MySQL 实例&#…

web安全及内网安全知识

本文来源无问社区&#xff08;wwlib.cn&#xff09;更多详细内容可前往观看http://www.wwlib.cn/index.php/artread/artid/7506.html Web安全 1、sql注入 Web程序中对于用户提交的参数未做过滤直接拼接到SQL语句中执行&#xff0c;导致参数中的特殊字符破坏了SQL语句原有逻…

简述多云互联原理,客户公私云池互通的产品是什么?

多云互联原理基于企业或组织使用多个不同的云服务提供商的基础设施和服务&#xff0c;以实现最佳的运营效率、弹性和成本效益。这种策略允许用户避免供应商锁定&#xff0c;分散风险&#xff0c;并利用不同云服务商的特定优势&#xff0c;例如价格、地理位置、功能或性能。 多云…

jvm 07 GC算法,内存池,对象内存分配

01 垃圾判断算法 1.1引用计数算法 最简单的垃圾判断算法。在对象中添加一个属性用于标记对象被引用的次数&#xff0c;每多一个其他对象引用&#xff0c;计数1&#xff0c; 当引用失效时&#xff0c;计数-1&#xff0c;如果计数0&#xff0c;表示没有其他对象引用&#xff0c;…

【LeetCode】字母异位词分组

目录 一、题目二、解法完整代码 一、题目 给你一个字符串数组&#xff0c;请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。 字母异位词 是由重新排列源单词的所有字母得到的一个新单词。 示例 1: 输入: strs [“eat”, “tea”, “tan”, “ate”, “nat”, …