Spring 事务使用详解

news2024/11/17 23:44:57

前言

什么是事务?根据 维基百科事务 介绍,数据库事务(简称:事务)是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。简单来说,事务就是将一系列操作当成一个不可拆分的执行逻辑单元,这些要么都成功,要么都失败。事务具有4个属性:原子性、一致性、隔离性、持久性。称为ACID特性。

Spring 事务

在使用 Spring 进行开发过程中,一般都会使用 Spring 来进行事务的控制,接下来就来看下 Spring 使用事务的详细过程,包括事务的传播方式等。本文根据 官方文档 的介绍,结合例子来进行说明。

Spring 事务支持两种方式,编程式事务和声明式事务,下面的栗子会使用声明式事务来举例,即使用 @Transactional 注解的方式.

栗子
首先来看个简单栗子,后面再来对事务的一些属性进行详细的分析和介绍。
1、
首先来看下两个数据库表结构 user 表和 address 表:

user 表:

在这里插入图片描述address表:

在这里插入图片描述

2、Spring 配置文件启动事务:

 1<tx:annotation-driven transaction-manager="transactionManager"/>
 2<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
 3    <property name="dataSource" ref="dataSource"/>
 4</bean>
 5<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
 6    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
 7    <property name="url" value="jdbc:mysql://localhost:3306/tsmyk"/>
 8    <property name="username" value="root"/>
 9    <property name="password" value="root"/>
10</bean>

3、定义需要事务执行的方法:

 1public class UserServiceImpl implements IUserService {
 2
 3    private JdbcTemplate jdbcTemplate;
 4    public void setDataSource(DataSource dataSource) {
 5        this.jdbcTemplate = new JdbcTemplate(dataSource);
 6    }
 7
 8    @Transactional
 9    @Override
10    public void add(User user) throws RuntimeException {
11        String sql = "insert into user(name, age, sex, money, job) values (?, ?, ?, ?, ?)";
12        Object[] args = new Object[]{user.getName(), user.getAge(),user.getSex(),user.getMoney(), user.getJob()};
13        jdbcTemplate.update(sql, args);
14        throw new RuntimeException("保存出现异常...");
15    }
16}

这里使用 JdbcTemplate 来操作数据库

4、在上述的配置文件中配置该 bean:

1<bean id="txUserService" class="main.tsmyk.transaction.UserServiceImpl">
2    <property name="dataSource" ref="dataSource"/>
3</bean>

5、单元测试:

 1@RunWith(SpringJUnit4ClassRunner.class)
 2@ContextConfiguration("/resources/myspring.xml")
 3public class TestTransaction {
 4
 5    @Autowired
 6    private IUserService userServiceImpl;
 7
 8    @Test
 9    public void test1() throws RuntimeException {
10        User user = new User("xiaoqi", 20, 1, 1000, "java");
11        userServiceImpl.add(user);
12    }
13}

6、运行结果会抛出异常:

在这里插入图片描述
7、此时数据库的数据还是原来的。

上述的栗子中,在 add() 方法加上了事务注解 @Transactional ,当该方法抛出异常的时候,数据库会进行回滚,数据插入失败。

事务的原理

Spring 事务是使用 AOP 来实现的,在执行目标方法之前和之后,我们可以进行一些增强操作,而这恰恰可以符合事务的使用情况,在目标方法执行成功后,提交事务,失败的时候,回滚事务。当然,我们还可以通过 AOP 来自定义事务的行为。从概念上讲,在事务代理上调用方法看起来如下所示:

在这里插入图片描述
当客户端调用的时候,调用的是代理对象,在执行目标方法之前,会创建事务,即事务增强,之后会执行我们自定义的增强行为(如果有的话,可以在事务增强之前或之后执行),之后执行目标方法,执行目标方法之后,又会执行自定义增强和事务增强,事务要么提交要么回滚,之后再返回给客户端;这就是它的一个流程,和 分析 AOP 的流程是一致的。

Spring 事务详解

事务只会对 public 方法有效,对 protected,private 和 package-visible 的方法,事务不会有效,在 proxy 模式下,如果是对象内部的方法自我调用,则调用的内部方法事务也不会生效.

下面通过栗子来验证下方法的自我调用看下事务是否生效

 1public class UserServiceImpl implements IUserService {
 2
 3    private JdbcTemplate jdbcTemplate;
 4    public void setDataSource(DataSource dataSource) {
 5        this.jdbcTemplate = new JdbcTemplate(dataSource);
 6    }
 7
 8    @Override
 9    public void add(User user) throws RuntimeException {
10        this.add_2(user);
11    }
12
13    @Transactional
14    @Override
15    public void add_2(User user) throws RuntimeException {
16        String sql = "insert into user(name, age, sex, money, job) values (?, ?, ?, ?, ?)";
17        Object[] args = new Object[]{user.getName(), user.getAge(),user.getSex(),user.getMoney(), user.getJob()};
18        jdbcTemplate.update(sql, args);
19        throw new RuntimeException("保存出现异常2...");
20    }
21}

现在 add() 方法调用 add_2() 方法,add() 方法不加事务,而 add_2() 方法添加了事务,此时客户端调用 add() 方法:

在这里插入图片描述
虽然 add_2() 方法抛出了异常,但是不会回滚,数据还是成功的插入:

在这里插入图片描述
这是为什么呢?因为 add() 方法通过 this 来调用 add_2() 方法,而 this 代表的是目标对象而不是代理对象,所以 this.add_2() 不会被代理,也就不会被事务控制,即事务不生效;这种情况下,怎么使 add_2() 也被事务进行控制呢?当然不能使用 this 来调用了,而是使用 代理对象 来调用: ((IUserService)AopContext.currentProxy()).add_2(user) ,但是,使用这种方式有个前提,需要把我们的代理对象暴露出来,放到 ThreadLocal 中,即在 配置文件 配置 expose-proxy 属性,即 <aop:aspectj-autoproxy expose-proxy=“true”/>。如下所示:

 1<aop:aspectj-autoproxy expose-proxy="true"/>
 2<tx:annotation-driven transaction-manager="transactionManager" mode="proxy"/>
 3@Override
 4public void add(User user) throws RuntimeException {
 5    ((IUserService)AopContext.currentProxy()).add_2(user);
 6}
 7
 8@Transactional
 9@Override
10public void add_2(User user) throws RuntimeException {
11    String sql = "insert into user(name, age, sex, money, job) values (?, ?, ?, ?, ?)";
12    Object[] args = new Object[]{user.getName(), user.getAge(),user.getSex(),user.getMoney(), user.getJob()};
13    jdbcTemplate.update(sql, args);
14    throw new RuntimeException("保存出现异常2...");
15}

再运行上面的测试代码,会发现,事务回滚了,数据插入失败。

如果我们在 add() 方法和 add_2() 方法都加上注解,且通过 this 来调用 add_2() 方法,事务会不会生效呢?

 1@Transactional
 2@Override
 3public void add(User user) throws RuntimeException {
 4    this.add_2(user);
 5}
 6
 7@Transactional
 8@Override
 9public void add_2(User user) throws RuntimeException {
10    String sql = "insert into user(name, age, sex, money, job) values (?, ?, ?, ?, ?)";
11    Object[] args = new Object[]{user.getName(), user.getAge(),user.getSex(),user.getMoney(), user.getJob()};
12    jdbcTemplate.update(sql, args);
13    throw new RuntimeException("保存出现异常2...");
14}

运行上面的测试代码,事务是生效的,这是因为它们属于同一个事务,且 add() 方法被事务进行管理。

@Transactional 注解可以放在接口上,接口方法上,类上,类的 public 方法上,但是 Spring 建议的是 @Transactional 尽量放在 类 或 类方法上,而不建议放在接口或接口方法;这是为什么呢?我们知道,事务是通过 Spring AOP 来实现的,而 Spring AOP 是通过动态代理来实现的,而 Spring 使用的动态代理主要有 JDK 动态代理和 CGLIB 代理,JDK 动态代理主要代理接口,这时 把 @Transactional 放在接口或接口方法上,事务是有效的;而 CGLIB 代理则是代理类,通过继承的方式来实现,这时把 @Transactional 放在接口或接口方法上,则事务就不会生效:

1@Transactional
2public interface IUserService{
3      void add(Useruser) throws RuntimeException;
4}

针对该配置方式,把事务注解放在接口上,则只会对 JDK 代理有效

1@Transactional
2@Override
3public void add(Useruser)  throws RuntimeException{
4    String sql="insert into user(name,age,sex,money,job) values(?,?,?,?,?)";
5    Object[] args=new Object[] {user.getName(),user.getAge(),user.getSex(),user.getMoney(),user.getJob()};
6    jdbcTemplate.update(sql,args);
7    throw new RuntimeException("保存出现异常...");
8}

把事务注解放在实现类上,则对 CGLIB 和 JDK 代理都有效
使用 CGLIB 代理,需要配置 proxy-target-class 属性为true:

<tx:annotation-driven proxy-target-class=“true”/>

<tx:annotation-driven/> 的属性

<tx:annotation-driven/> 标签用来表示开启事务功能,它有 4 个属性:

<tx:annotation-driven transaction-manager="transactionManager" mode="proxy" proxy-target-class="true" order="1"/>
  • transaction-manager :事务管理器的名称,默认为 transactionManager,因为可以不写,如果管理器的名称不是这个才需要写。

  • mode : 模式,两种,proxy 模式和 aspectj 模式,proxy 仅适用于通过代理进入的方法调用,aspectj 适用于任何类型的方法调用

  • proxy-target-class : 使用 CGLIB 进行代理,代理类而不是代理接口

  • order:代理顺序

@Transactional 属性

@Transactional 有很多属性来控制事务的行为,共 9 个属性

属性类型描述默认值
valueString该事务对应的事务管理器transactionManager
propagation枚举:Propagation事务传播方式REQUIRED
isolation枚举: Isolation事务隔离级别DEFAULT
readOnlyboolean读/写 与 只读false
timeoutint(以秒为单位)超时时间-1
rollbackForClass对象数组需要回滚的异常空数组 {}
rollbackForClassName类名数组需要回滚的异常类名空数组 {}
noRollbackForClass对象数组不需要回滚的异常空数组 {}
noRollbackForClassName类名数组不需要回滚的异常类名空数组 {}

事务的名称就是方法的全限定名,无法设置

事务的传播方式

接下来看下事务的传播方式,事务的传播方式在 Spring 事务中非常重要,需要理解清楚,否则有时候事务不回滚不知道问题出在哪里。

事务的传播方式使用 propagation 属性来表示,它是一个枚举类型,共有 7 个,即事务的传播方式有 7 种:

1public enum Propagation {    
2    REQUIRED //required:需要事务,如果事务不存在,则创建一个新事务    
3    REQUIRES_NEW //required_new:需要创建一个新事务,如果已存在事务,则把当前事务挂起  
4    NESTED //nested:嵌套事务
5    SUPPORTS //supports:支持事务,如果没有事务,则以非事务的方式运行    
6    NOT_SUPPORTED //not_supported:不支持事务,以非事务的方式运行,如果存在事务,则挂起    
7    NEVER //never:不支持事务,如果存在事务,则抛出异常    
8    MANDATORY //mandatory:支持事务,如果没有事务,则抛出异常 
9}

下面以栗子的方式来验证这几种传播方式,数据库的相关表结构还是文章开头的 user表和 address 表,代码如下:

 1public class UserServiceImpl implements IUserService {
 2    private JdbcTemplate jdbcTemplate;
 3    public void setDataSource(DataSource dataSource) {this.jdbcTemplate = new JdbcTemplate(dataSource);}
 4
 5    @Autowired
 6    private IAddressService addressService;
 7
 8    @Override
 9    public void addUser(User user) throws RuntimeException {
10        String sql = "insert into user(name, age, sex, money, job) values (?, ?, ?, ?, ?)";
11        Object[] args = new Object[]{user.getName(), user.getAge(),user.getSex(),user.getMoney(), user.getJob()};
12        jdbcTemplate.update(sql, args);
13        addressService.addAddress(3, "shanghai");
14    }
15}
16
17public class AddressServiceImpl implements IAddressService {
18    private JdbcTemplate jdbcTemplate;
19    public void setDataSource(DataSource dataSource) {this.jdbcTemplate = new JdbcTemplate(dataSource);}
20
21    @Override
22    public void addAddress(int id, String name) throws RuntimeException{
23        String sql = "insert into address(id, name) values (?, ?)";
24        Object[] args = new Object[]{id, name};
25        jdbcTemplate.update(sql, args);
26        throw new RuntimeException("保存Address出现异常...");
27    }
28}
29
30// 测试代码
31@Test
32public void testUser() throws RuntimeException {
33    User user = new User("xiaoqi", 20, 1, 1000, "java");
34    userServiceImpl.addUser(user);
35}

即就是在 UserServiceImpl 的 addUser() 方法里面调用 AddressServiceImpl 的 addAddress() 方法

REQUIRED

required 这种传播方式,它是需要在事务中运行的,如果事务不存在,则创建一个新事务
在 addAddress() 方法加上注解 @Transactional(propagation = Propagation.REQUIRED),如下所示:

1public void addUser(User user) throws RuntimeException {
2 String sql = “insert into user(name, age, sex, money, job) values (?, ?, ?, ?, ?)”;
3 Object[] args = new Object[]{user.getName(), user.getAge(),user.getSex(),user.getMoney(), user.getJob()};
4 jdbcTemplate.update(sql, args);
5 addressService.addAddress(3, “shanghai”);
6}
7
8@Transactional(propagation = Propagation.REQUIRED)
9public void addAddress(int id, String name) throws RuntimeException{
10 String sql = “insert into address(id, name) values (?, ?)”;
11 Object[] args = new Object[]{id, name};
12 jdbcTemplate.update(sql, args);
13 throw new RuntimeException(“保存Address出现异常…”);
14}
因为 addUser 没要添加事务,所以 addAddress 会创建一个新事务,运行上面的测试代码,日志如下(IDEA 把 log4j 日志输出到文件查看):

在这里插入图片描述
可以看到,第一步,执行user插入操作,插入成功,并没有创建事务,第二步,创建事务,事务名称为 main.tsmyk.transaction.AddressServiceImpl.addAddress,执行 address 插入,插入成功,又因为 address 抛出异常,所以 address 插入进行回滚,回滚的数据库连接是 515809288,即执行 address 插入的连接,并没有回滚 user 插入的连接,所以结果是 user 正常插入,而 address 插入失败。为什么addUser 没有进行回滚呢,因为 它又没有在事务中运行,自然就不会回滚了。

在 addUser 和addAddress 方法都加上注解 @Transactional(propagation = Propagation.REQUIRED),如下所示:

 1@Transactional(propagation = Propagation.REQUIRED)
 2public void addUser(User user) throws RuntimeException {
 3    String sql = "insert into user(name, age, sex, money, job) values (?, ?, ?, ?, ?)";
 4    Object[] args = new Object[]{user.getName(), user.getAge(),user.getSex(),user.getMoney(), user.getJob()};
 5    jdbcTemplate.update(sql, args);
 6    addressService.addAddress(3, "shanghai");
 7}
 8
 9@Transactional(propagation = Propagation.REQUIRED)
10public void addAddress(int id, String name) throws RuntimeException{
11    String sql = "insert into address(id, name) values (?, ?)";
12    Object[] args = new Object[]{id, name};
13    jdbcTemplate.update(sql, args);
14    throw new RuntimeException("保存Address出现异常...");
15}

运行测试代码,日志如下:

在这里插入图片描述
可以看到,首先会创建事务,名称为 addUser 的全限定名,获取数据库连接 418958713,之后会在该连接中执行 user 和 address 的插入操作,即在同一个事务中,address 插入抛出异常,进行回滚,回滚的是连接 418958713,所以 user 和 address 的操作都会被回滚,都插入失败。

如果在 addUser 调用 addAddress 的时候,进行异常的捕获,addUser 会进行回滚嘛?如下:

 1@Transactional(propagation = Propagation.REQUIRED)
 2public void addUser(User user) throws RuntimeException {
 3    String sql = "insert into user(name, age, sex, money, job) values (?, ?, ?, ?, ?)";
 4    Object[] args = new Object[]{user.getName(), user.getAge(),user.getSex(),user.getMoney(), user.getJob()};
 5    jdbcTemplate.update(sql, args);
 6    try {
 7        addressService.addAddress(3, "shanghai");
 8    }catch (Exception e){}
 9}
10
11@Transactional(propagation = Propagation.REQUIRED)
12public void addAddress(int id, String name) throws RuntimeException{
13    String sql = "insert into address(id, name) values (?, ?)";
14    Object[] args = new Object[]{id, name};
15    jdbcTemplate.update(sql, args);
16    throw new RuntimeException("保存Address出现异常...");
17}

运行日志如下:

在这里插入图片描述
可以看到,它们还是在同一个事务中运行,同一个连接中进行插入,回滚的是同一个连接,所以都会插入失败,即使进行了异常捕获。

总结:所以 REQUIRED 这种传播方式,必须要在事务中运行,不存在事务,则创建一个,即使进行的异常的捕获,外部还是会进行回滚,这是因为虽然在每个方法都加上了事务注解,看起来是独立的事务,可是都会映射到底层数据库中的同一个物理事务中,所以只要有一个进行了回滚,则都会进行回滚。

REQUIRES_NEW

required_new,需要创建一个新事务,如果已存在事务,则把当前事务挂起
在 addUser 方法加上注解 @Transactional(propagation = Propagation.REQUIRED)
在 addAddress 方法加上注解 @Transactional(propagation = Propagation.REQUIRES_NEW)

 1@Transactional(propagation = Propagation.REQUIRED)
 2public void addUser(User user) throws RuntimeException {
 3    String sql = "insert into user(name, age, sex, money, job) values (?, ?, ?, ?, ?)";
 4    Object[] args = new Object[]{user.getName(), user.getAge(),user.getSex(),user.getMoney(), user.getJob()};
 5    jdbcTemplate.update(sql, args);
 6    addressService.addAddress(3, "shanghai");
 7}
 8
 9@Transactional(propagation = Propagation.REQUIRES_NEW)
10public void addAddress(int id, String name) throws RuntimeException{
11    String sql = "insert into address(id, name) values (?, ?)";
12    Object[] args = new Object[]{id, name};
13    jdbcTemplate.update(sql, args);
14    throw new RuntimeException("保存Address出现异常...");
15}

运行日志如下:

在这里插入图片描述
可以看到,创建了两个事务,获取了两个数据库连接 1164799006 和 418958713,当执行 address 插入的时候,会把已经存在的事务挂起,新创建一个事务进行运行,当 address 插入抛出异常的时候,这两个事务都进行了回滚,所以都会插入失败。

在 addUser 调用 addAddress 的时候,进行异常捕获,则会怎么样呢?

 1@Transactional(propagation = Propagation.REQUIRED)
 2public void addUser(User user) throws RuntimeException {
 3    String sql = "insert into user(name, age, sex, money, job) values (?, ?, ?, ?, ?)";
 4    Object[] args = new Object[]{user.getName(), user.getAge(),user.getSex(),user.getMoney(), user.getJob()};
 5    jdbcTemplate.update(sql, args);
 6    try {
 7        addressService.addAddress(3, "shanghai");
 8    }catch (Exception e){}
 9}
10
11@Transactional(propagation = Propagation.REQUIRES_NEW)
12public void addAddress(int id, String name) throws RuntimeException{
13    String sql = "insert into address(id, name) values (?, ?)";
14    Object[] args = new Object[]{id, name};
15    jdbcTemplate.update(sql, args);
16    throw new RuntimeException("保存Address出现异常...");
17}

运行日志如下:

在这里插入图片描述
可以看到,还是创建两个事务,获取了两个连接,进行异常捕获了之后,只会回滚一个事务,

总结:REQUIRES_NEW 它是创建了一个新的事务进行运行,它们是完全独立的事务范围,对应到底层数据库的物理事务也是不同的,所以它们可以独立提交或回滚,外部事务不受内部事务的回滚状态的影响; 对于上述栗子来说,如果 addAddress 抛异常且 addUser 不进行异常捕获,则两个事务都会进行回滚,如果 addUser 进行了异常捕获,则 addUser 可以进行提交的,它们是两个独立的事务;如果 addAddress 执行成功,外层的 addUser 执行失败,则 addUser 会回滚,则内部执行成功的 addAddress 不会回滚。

NESTED

nested,嵌套事务,它是外部事务的一个子事务,新建一个子事务进行运行;它们并不是独立,如果外部事务提交,则嵌套事务也会提交,外部事务回滚,则嵌套事务也会回滚。嵌套事务主要支持 saveponit 保存点。

在addUser方法加上注解@Transactional(propagation = Propagation.REQUIRED)
在addAddress方法加上注解@Transactional(propagation = Propagation.NESTED)

 1@Transactional(propagation = Propagation.REQUIRED)
 2public void addUser(User user) throws RuntimeException {
 3    String sql = "insert into user(name, age, sex, money, job) values (?, ?, ?, ?, ?)";
 4    Object[] args = new Object[]{user.getName(), user.getAge(),user.getSex(),user.getMoney(), user.getJob()};
 5    jdbcTemplate.update(sql, args);
 6    addressService.addAddress(3, "shanghai");
 7}
 8
 9@Transactional(propagation = Propagation.NESTED)
10public void addAddress(int id, String name) throws RuntimeException{
11    String sql = "insert into address(id, name) values (?, ?)";
12    Object[] args = new Object[]{id, name};
13    jdbcTemplate.update(sql, args);
14    throw new RuntimeException("保存Address出现异常...");
15}

运行日志如下:

在这里插入图片描述
可以看到,新建了两个事务,一个是 nested 嵌套事务,而且只是获取了一个数据库连接 418958713,在同一个连接中执行两条SQL,当 addAddress 出现异常进行回滚的时候,只是回滚到 savepoint 保存到,由于外层的 addUser 没有进行异常捕获,所以外部事务回滚,即回滚连接 418958713。

如果在外层进行异常捕获,则外层的 addUser会插入成功嘛?

 1@Transactional(propagation = Propagation.REQUIRED)
 2public void addUser(User user) throws RuntimeException {
 3    String sql = "insert into user(name, age, sex, money, job) values (?, ?, ?, ?, ?)";
 4    Object[] args = new Object[]{user.getName(), user.getAge(),user.getSex(),user.getMoney(), user.getJob()};
 5    jdbcTemplate.update(sql, args);
 6    try {
 7        addressService.addAddress(3, "shanghai");
 8    }catch (Exception e){}
 9}
10
11@Transactional(propagation = Propagation.NESTED)
12public void addAddress(int id, String name) throws RuntimeException{
13    String sql = "insert into address(id, name) values (?, ?)";
14    Object[] args = new Object[]{id, name};
15    jdbcTemplate.update(sql, args);
16    throw new RuntimeException("保存Address出现异常...");
17}

运行日志如下:

在这里插入图片描述
可以看到,内层的事务会回滚到 savepoint 保存点,而外层的事务则可以正常提交,结果就是 address 插入失败,user 插入成功。
现在如果内层的 addAddress 执行成功,不抛异常,外层的 addUser 抛异常,则 内层的 addAddress 会回滚嘛?

 1@Transactional(propagation = Propagation.REQUIRED)
 2public void addUser(User user) throws RuntimeException {
 3    String sql = "insert into user(name, age, sex, money, job) values (?, ?, ?, ?, ?)";
 4    Object[] args = new Object[]{user.getName(), user.getAge(),user.getSex(),user.getMoney(), user.getJob()};
 5    jdbcTemplate.update(sql, args);
 6    addressService.addAddress(3, "shanghai");
 7    throw new RuntimeException("保存User出现异常...");
 8}
 9
10@Transactional(propagation = Propagation.NESTED)
11public void addAddress(int id, String name) throws RuntimeException{
12    String sql = "insert into address(id, name) values (?, ?)";
13    Object[] args = new Object[]{id, name};
14    jdbcTemplate.update(sql, args);
15}

运行日志如下:

在这里插入图片描述
如果外部事务回滚了,内部事务也会回滚,因为它们属于同一个底层数据库的物理事务。

总结:嵌套事务, 它是已经存在事务的子事务. 嵌套事务开始执行时, 它将取得一个savepoint. 如果这个嵌套事务失败, 将回滚到此savepoint. 嵌套事务是外部事务的一部分, 只有外部事务结束后它才会被提交. 使用NESTED 有限制,它只支持 JDBC,且数据库要支持 savepoint 保存点,还要 JDBC 的驱动在3.0以上

SUPPORTS

supports,支持事务,如果没有事务,则以非事务的方式运行

NOT_SUPPORTED

not_supported,不支持事务,以非事务的方式运行,如果存在事务,则挂起

NEVER

never,不支持事务,如果存在事务,则抛出异常

MANDATORY

mandatory,支持事务,如果没有事务,则抛出异常

这几种传播方式比较好理解,就不把栗子贴出来了。

事务的隔离级别

事务的隔离级别使用 isolation 表示,它是一个枚举 Isolation

1public enum Isolation {
2
3    DEFAULT // 默认的隔离级别,和底层数据库有关,MySQL 是可重复读(Repeated Read)
4    READ_UNCOMMITTED // 读未提交,允许脏读,可能读取到其他会话中未提交事务修改的数据
5    READ_COMMITTED // 读已提交,只能读取到已经提交的数据
6    REPEATABLE_READ // 可重复读,该隔离级别消除了不可重复读,但是还存在幻象读
7    SERIALIZABLE // 序列化,完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞
8
9}

总结

  1. Spring 事务原理:

Spring 事务是通过 Spring AOP 来实现的,还可以通过 AOP 来自定义事务的行为。

  1. 事务注解 @Transactional

事务注解 @Transactional 可以放在接口上,接口方法上,类上,类的public方法上,如果放在接口或接口方法上,则只会对 JDK 代理有效,对 CGLIB 代理无效,我们知道,事务是通过 Spring AOP 来实现的,而 Spring AOP是通过动态代理来实现的,而Spring 使用的动态代理主要有 JDK 动态代理和 CGLIB 代理,JDK 动态代理主要代理接口,这时 把 @Transactional 放在接口或接口方法上,事务是有效的;而 CGLIB 代理则是代理类,通过继承的方式来实现,这时把 @Transactional 放在接口或接口方法上,则事务就不会生效的。

  1. 内部调用事务不生效的解决方法

一是把该方法放到其他的对象中,不过不太实用,二是不通过 this 来调用方法,而是通过代理来调用,如 AopContext.currentProxy().xxxx ,但是,使用这种方式有个前提,需要把我们的代理对象暴露出来,放到 ThreadLocal 中,即在 配置文件配置 expose-proxy 属性,即<aop:aspectj-autoproxy expose-proxy=“true”/>

  1. 事务的传播方式,7 种
1REQUIRED //required:需要事务,如果事务不存在,则创建一个新事务    
2REQUIRES_NEW //required_new:需要创建一个新事务,如果已存在事务,则把当前事务挂起    
3NESTED //nested:嵌套事务
4SUPPORTS //supports:支持事务,如果没有事务,则以非事务的方式运行    
5NOT_SUPPORTED //not_supported:不支持事务,以非事务的方式运行,如果存在事务,则挂起    
6NEVER //never:不支持事务,如果存在事务,则抛出异常    
7MANDATORY //mandatory:支持事务,如果没有事务,则抛出异常  

REQUIRED : 必须要在事务中运行,不存在事务在,则创建一个,即使进行的异常的捕获,外部还是会进行回滚,这是因为虽然在每个方法都加上了事务注解,看起来是独立的事务,可是都会映射到底层数据库中的同一个物理事务中,所以只要有一个进行了回滚,则都会进行回滚。

REQUIRES_NEW 它是创建了一个新的事务进行运行,它们是完全独立的事务范围,对应到底层数据库的物理事务也是不同的,所以它们可以独立提交或回滚,外部事务不受内部事务的回滚状态的影响.

NESTED 嵌套事务, 它是已经存在事务的子事务. 嵌套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 将回滚到此 savepoint. 嵌套事务是外部事务的一部分, 只有外部事务结束后它才会被提交. 使用 NESTED 有限制,它只支持 JDBC,且数据库要支持 savepoint 保存点,还要 JDBC 的驱动在3.0以上。

  1. 事务的隔离级别

1DEFAULT // 默认的隔离级别,和底层数据库有关,MySQL 是可重复读(Repeated Read)
2READ_UNCOMMITTED // 读未提交,允许脏读,可能读取到其他会话中未提交事务修改的数据
3READ_COMMITTED // 读已提交,只能读取到已经提交的数据
4REPEATABLE_READ // 可重复读,该隔离级别消除了不可重复读,但是还存在幻象读
5SERIALIZABLE // 序列化,完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞

事务不生效

事务不生效问题经常出现在我们使用 Spring 声明式事务中,主要原因是因为对 Spring 事务管理机制和 声明式事务实现原理不够了解所致,下面我们一起来看下常见误区:

Case1:类内部访问

场景描述:A 类的 a1 方法没有标注 @Transactional,a2 方法标注 @Transactional,在 a1里面调用 a2;事务不生效。

举例说明:

@Service
public class TestService {

    public void test(){
        doSomething1();
    }

    @Transactional
    public void doSomething1(){
        // 数据库操作
    }
}

在 test() 方法中调用被 @Transaction 注解标注的 doSomething1() 方法

@RestController
public class TestController {

    @Resource
    private TestService testService;

    @GetMapping("/test")
    public void test() {
        testService.test();
    }
}

当进行调用 testService.test() 时,doSomething1() 方法的事务不会生效。

原因解释:
因为 @Transactional 的工作机制是基于 AOP 实现,AOP 是使用动态代理实现的。如果通过代理调用doSomething1(),通过 AOP 会前后进行增强,增强的逻辑其实就是在 doSomething1() 的前后别加上开启、提交事务的逻辑。

而现在是在 test() 方法中直接调用 this.doSomething1(),this 指向 TestService 的非代理对象,不会经过 AOP 的事务增强操作。也就是类内部调用事务方法,不会通过代理方式访问。

解决方法也很简单,这里介绍一个通用的处理方式,那就是在 本类中获取 Spring IoC 容器,使用依赖查找的方式从容器中直接获取 当前代理对象。如下:

@Service
public class TestService implements ApplicationContextAware {

    private ApplicationContext applicationContext;
    
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
    
    public void test() {
        // 从Spring IoC 容器中获取代理类
        TestService testService = applicationContext.getBean(TestService.class);
        // 调用代理类目标方法
        testService.doSomething1();
    }

    @Transactional
    public void doSomething1() {
        // 数据库操作
        System.out.println("doSomething1 数据库操作。。。");
    }
}

除了上述方法,我们还可以 通过 AopContext 获取代理对象来调用:

@Service
public class TestService {
    
    public void test() {        
       // 获取当前类的代理对象
       TestService testServiceProxy = ((TestService)AopContext.currentProxy())
        // 调用代理类目标方法
       testServiceProxy.doSomething1();
    }

    @Transactional
    public void doSomething1() {
        // 数据库操作
        System.out.println("doSomething1 数据库操作。。。");
    }
}

Case2:非 public 可重载方法

场景描述:
在 Spring Bean 的非 public 方法上,添加 @Transactional 注解不会生效。

举例说明:

@Service
public class TestService {
    
    @Transactional
    protected void doSomething2(){
        // 数据库操作
    }

    @Transactional
    public final void doSomething3(){
        // 数据库操作
    }

    @Transactional
    public static void doSomething4(){
        // 数据库操作
    }
}

doSomething2() 被 protected 修饰符修饰,导致事务不会生效;
doSomething3() 被 final 修饰,导致事务不生效;
doSomething4() 被 static 修饰,导致事务不会生效。

@RestController
public class TestController {

    @Resource
    private TestService testService;

    @GetMapping("/test")
    public void test() {
        testService.doSomething2();
        testService.doSomething3();
        testService.doSomething4();
    }
}

实际使用时,这三种场景不太容易出现,因为 IDEA 会有提醒: 被 @Transactional 标注的方法必须可被重载。

在这里插入图片描述原因解释:
Spring 要求被代理方法必须是 public 的并且是可被重载的。至于深层次的原理,源码部分会给你解读。

Case3:异常不匹配(重点)

场景描述:
@Transactional 没有设置 rollbackFor = Exception.class 属性或者指定异常类型和方法抛出的异常类型不匹配。

举例说明:

@Service
public class TestService {

    @Transactional
    public void doSomething5() throws Exception {
        // 数据库操作
        throw new Exception("发生异常");
    }
}

原因解释:
Spring 事务默认只捕获 uncheck exception,即只会回滚 RuntimeException(运行时异常)和Error(错误),对于普通的 Exception(非运行时异常),它不会回滚。
在这里插入图片描述

Case4:多线程

场景描述:
在 Spring 事务方法中开启线程操作数据库,不受主线程事务控制。

举例说明:
下面给出两个不同的Case,一个是子线程 ok,父线程抛异常;一个是子线程抛异常,主线程 ok。

    @Transactional
    public  void doSomething6() {
        // 数据库操作

        //  开启子线程
        new Thread(() -> {
           doDb();
        }).start();

        // 主线程异常
        throw new RuntimeException("父线程异常");
    }

父线程抛出线程,事务回滚。因为子线程是独立存在,和父线程不在同一个事务中,所以子线程的修改并不会被回滚。

    @Transactional
    public  void doSomething7() {
        // 数据库操作

        //  开启子线程
        new Thread(() -> {
            doDb();
            throw new RuntimeException("子线程异常");
        }).start();
    }

由于子线程的异常不会被外部的线程捕获,所以父线程不抛异常,事务回滚没有生效。

原因解释:
Spring事务控制是通过 AOP 动态代理,在方法开始是开启一个事务,并把该事务放在当前前程上下文中进行传递,开启子线程事务无法传递。导致子线程事务失效。至于深层次的原理,源码部分会给你解读。

Case5:嵌套事务未捕获异常

Spring 传播属性中的嵌套事务是通过 JDBC 提供 SavePoint 来实现的。PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED 比较容易混淆:

PROPAGATION_REQUIRES_NEW:启动一个新的, 不依赖于环境的 “内部” 事务. 这个事务将被完全 commited 或 rolled back 而不依赖于外部事务, 它拥有自己的隔离范围, 自己的锁, 等等. 当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行。PROPAGATION_NESTED: 开始一个 “嵌套的” 事务, 它是已经存在事务的一个真正的子事务.潜套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 我们将回滚到此savepoint. 嵌套事务是外部事务的一部分, 只有外部事务结束后它才会被提交。如果外部事务 commit, 嵌套事务也会被 commit, 这个规则同样适用于 rollback。

场景描述:
一个事务方法A调用另一个嵌套事务方法B,嵌套事务方法A 异常,不希望影响 事务方法B

举例说明:

public class TestService {

    @Resource
    private TagRepository tagRepository;

       // 默认隔离级别
    @Transactional(propagation = Propagation.REQUIRED)
    public void doSomething8() {
        tagRepository.upsert("doSomething8", TagType.UNKNOWN);
        ((TestService) AopContext.currentProxy()).doSomething9();
    }

    @Transactional(propagation = Propagation.NESTED, rollbackFor = RuntimeException.class)
    public void doSomething9() {
        tagRepository.upsert("doSomething9", TagType.UNKNOWN);
          throw new RuntimeException("doSomething9 exception");
    }
}

这种情况使用了嵌套的内部事务,原本是希望调用 doSomething9()方法时,如果出现了异常,只回滚doSomething9()方法里的内容,不回滚 doSomething8里的内容,即回滚保存点。但实际上,doSomething8 也回滚了。

原因解释:
因为doSomething9方法出现了异常,没有手动捕获,会继续往上抛,到外层 doSomething8 方法的代理方法中捕获了异常。所以,这种情况是直接回滚了整个事务,不只回滚单个保存点。

可以将内部嵌套事务放在 try/catch 中,并且不继续往上抛异常。这样就能保证,如果内部嵌套事务中出现异常,只回滚内部事务,而不影响外部事务。

    // 默认隔离级别
    @Transactional(propagation = Propagation.REQUIRED)
    public void doSomething8() {
        tagRepository.upsert("doSomething8", TagType.UNKNOWN);
        try {
            ((TestService) AopContext.currentProxy()).doSomething9();
        } catch (Exception e) {
            System.out.println("捕获 doSomething9 异常");
        }
    }

这种方式也是嵌套事务最有价值的地方, 它起到了分支执行的效果。嵌套事务执行异常,自动回滚到保存点,在外部事务捕获异常,做其他逻辑处理。

Case6:事务传播属性使用理解不当

场景描述:
事务方法A 调用事务方法B,期望事务B回滚,事务A也回滚。

举例说明:

    // 默认隔离级别
    @Transactional(propagation = Propagation.REQUIRED)
    public void doSomething8() {
        tagRepository.upsert("doSomething8", TagType.UNKNOWN);

        applicationContext.getBean(TestService.class).doSomething9();
        throw new RuntimeException("doSomething9 exception");
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = RuntimeException.class)
    public void doSomething9() {
        tagRepository.upsert("doSomething9", TagType.UNKNOWN);
    }

这种情况下,doSomething9 事务为 PROPAGATION_REQUIRES_NEW ,所以与PROPAGATION_REQUIRED 不会有任何关系。不会因为对方的执行情况影响事务的结果,因为他们根本就是两个事务。

原因解释:
PROPAGATION_REQUIRES_NEW 配置下,当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行。外部事务异常不会影响内部事务。

Case7:自己吞掉了异常

场景描述:
在事务方法中 try…catch 住异常没有向外抛出,导致事务不生效。

举例说明:

    @Transactional
    protected void test(){
        // 数据库操作
        try {
            doOther();
        } catch (Exception e) {
            log.error("xxx");
        }
    }

原因解释:
Spring 事物处理是通过 AOP try…catch 代理方法,根据异常信息来回滚事物的,如果没检测到异常,也便不会回滚事物了。

大事务问题

@Transactional注解一般加在某个业务方法上,会导致整个业务方法都在同一个事务中,粒度太粗,不好控制事务范围,是出现大事务问题的最常见的原因。

大事务常见问题?
所谓大事务就是事务流程较长的事务,大事务会造成许多性能问题:

  • 死锁
  • 锁定太多的数据,造成大量的阻塞和锁超时
  • 执行时间长,容易造成主从延迟
  • 回滚时间长
  • 并发情况下,数据库连接池容易被占满
  • undo log日志膨胀,不仅增加了存储的空间,而且可能降低查询的性能

最主要的影响数据库连接池容易被撑爆,导致大量线程等待,造成请求无响应或请求超时。

如何查询大事务?

select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>10

如何避免大事务问题?
这里我总结了如下几点:

1、少用 @Transactional 注解
@Transactional注解一般加在某个业务方法上,会导致整个业务方法都在同一个事务中,粒度太粗,不好控制事务范围,是出现大事务问题的最常见的原因。可以使用我们上面介绍到的编程式事务,在spring项目中使用 TransactionTemplate 手动执行事务。

2、将查询方法放到事务外
如果出现大事务,可以将查询(select)方法放到事务外,也是比较常用的做法,因为一般情况下这类方法是不需要事务的。如下面代码:

@Transactional(rollbackFor=Exception.class) 
   public void save(User user) { 
         queryData1(); 
         queryData2(); 
         addData1(); 
         updateData2(); 
   } 

可以将queryData1和queryData2两个查询方法放在事务外执行,将真正需要事务执行的代码才放到事务中,比如:addData1和updateData2方法,这样就能有效的减少事务的粒度。

该怎么拆分呢? 多数人可能会想到下面这样:

public void save(User user) { 
         queryData1(); 
         queryData2(); 
         doSave(); 
    } 
    
    @Transactional(rollbackFor=Exception.class) 
    public void doSave(User user) { 
       addData1(); 
       updateData2(); 
    } 

这个例子是非常经典的错误,这种直接方法调用的做法事务不会生效,这和我们前面在 Spring 事务不生效一章中介绍的 Case1 一模一样,这种错误经常会在日常开发中出现。即使有经验的开发人员稍不注意,就会采坑。

因此,我们更推荐使用下面方法重构实现:

@Autowired 
   private TransactionTemplate transactionTemplate; 
    
   public void save(final User user) { 
         queryData1(); 
         queryData2(); 
         transactionTemplate.execute((status) => { 
            addData1(); 
            updateData2(); 
            return Boolean.TRUE; 
         }) 
   } 

3、事务中避免远程调用
我们在接口中调用其他系统的接口是不能避免的,由于网络不稳定,这种远程调的响应时间可能比较长,如果远程调用的代码放在某个事务中,这个事务就可能是大事务。当然,远程调用不仅仅是指调用接口,还有包括:发MQ消息,或者连接redis、mongodb保存数据等。

@Transactional(rollbackFor=Exception.class) 
   public void save(User user) { 
         callRemoteApi(); 
         addData1();
         sendMq(); 
   } 

远程调用的代码可能耗时较长,切记一定要放在事务之外。

   @Resource
   private TransactionTemplate transactionTemplate; 
    
   public void save(User user) { 
         callRemoteApi(); 
         transactionTemplate.execute((status) => { 
            addData1(); 
            return Boolean.TRUE; 
         }) ;
         sendMq();
   } 

有些朋友可能会问,远程调用的代码不放在事务中如何保证数据一致性呢?这就需要建立:重试+补偿机制,达到数据最终一致性了。

4、事务中避免一次性处理太多数据
如果一个事务中需要处理的数据太多,也会造成大事务问题。比如为了操作方便,你可能会一次批量更新1000条数据,这样会导致大量数据锁等待,特别在高并发的系统中问题尤为明显。

    @Transactional(rollbackFor = Exception.class)
    public void saveList(List<User> userList) {
        insert(userList);
    }

解决办法是分页处理,1000条数据,分50页,一次只处理20条数据,这样可以大大减少大事务的出现。

    public void saveList(List<User> userList) {
        Lists.partition(userList, 20)
                .forEach(users -> {
                    transactionTemplate.execute(transactionStatus -> {
                        return insert(users);
                    });
                });
    }

Spring 事务使用最佳实践

Spring 声明式事务适合简单的事务场景,使用方便,但是容易采坑,当使用 Spring 声明式事务时,请仔细检查确认一下几个方面:

  • @Transactional 注解必须明确指定回滚异常 rollbackFor = XXX.class
  • 该事物方法必须是通过代理类访问
  • 确保异常被抛出

对于一些不便于使用 声明式事务的场景,我们推荐使用 编程式事物管理来控制事务。 使用编程式事务管理几乎不需要注意什么,编程上事务可以让我们更加细粒度的控制事务范围,并且可以很好的避免声明式事物带来的一些坑。

编程式事务使用最佳实践

编程式事务虽然使用起来相比声明式事务有些繁琐,但是借助于 TransactionTemplate 我们也能很容易的实现事务控制逻辑。这里我们建议,尽量使用 编程式事务 TransactionTemplate 替代声明式事务使用。

原理分析

源码分析

Spring的事务框架将开发过程中事务管理相关的关注点进行适当的分离,通过Spring的事务框架,我们可以按照统一的编程模型来进行事务编程。
下面我们简要分析下其中关键源码,想要系统了解 Spring 抽象事务管理器可以参考这篇文章:
Spring 三部曲(三):Spring 的 数据访问

Spring 事务的设计理念的基本原则是:让 事务管理 与 数据访问,相分离。Spring 把事务管理当做一种横切关注点,基于 Spring AOP 提供了一个事务拦截器 TransactionInterceptor ,在业务方法执行开始之前开启一个事务,当方法执行完成或者异常退出的时候就提交事务或者回滚事务。在拦截器方法: org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction 中处理事务逻辑。

在这里插入图片描述
第二步是判断是声明式事务还是编程式事务,else 逻辑走编程式事务处理:

在这里插入图片描述
非 public 方法导致事务不生效原因
上面分析到,拦截器调用了方法 getTransactionAttribute(),主要是为了获取 txAttr 变量,它是用于读取 @Transactional 的配置,如果这个 txAttr = null,后面就不会走事务逻辑,我们看一下这个变量的含义:

在这里插入图片描述
直接进入 getTransactionAttribute(),重点关注获取事务配置的方法。

在这里插入图片描述

在这里插入图片描述
异常不匹配原因
我们继续回到事务的核心逻辑,因为主方法抛出 Exception() 异常,进入事务回滚的逻辑:

在这里插入图片描述
当没有设置 rollbackFor 属性时,默认只对 RuntimeException 和 Error 的异常执行回滚。

这里只对关键源码进行简要分析,如果想了解更多关于 Spring 事务处理的细节,可以自行 debug,走一遍 Spring 事务处理流程,将会更加印象深刻。1

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

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

相关文章

报喜鸟如何触发数字化转型及品牌扩张战略分析

传统服装企业往往面临缺乏创新、盲目扩张、追求低成本、库存和行业周期性等问题。报喜鸟通过深入分析市场需求&#xff0c;明确聚焦主业&#xff0c;提出加强品牌核心竞争力的价值主张。通过实施DTC转型&#xff0c;发力电商平台和线下门店等举措&#xff0c;报喜鸟成功提高品牌…

VSC++=》 指针实数排序

缘由https://bbs.csdn.net/topics/396523482 void 指针实数排序(double* aa, int d) {//缘由https://bbs.csdn.net/topics/396523482double lin 0; int j d, jj 0;while (jj < d) if (--j > jj) if (aa[j] > aa[j - 1])lin aa[j], aa[j] aa[j - 1], aa[j - 1] …

Flask request和requests(客户端服务器)

Flask request和requests 1、Flask request属性2、requests属性3、实现代码 1、Flask request属性 这么多属性什么时候有值什么时候没值&#xff0c;其实完全取决于我们请求头content-type是什么&#xff0c;如果是以表单形式multipart/form-data、application/x-www-form-url…

辅助驾驶功能开发-功能规范篇(21)-3-XP行泊一体方案功能规范

XPilot Parking 自动泊车系统 七、全自动泊车(AutoParking) • 自动泊车辅助(AutoParking Assist)、斜列式车位泊车辅助(Diagonal AutoParking Assist) - 产品定义 基于超声波传感器和环视摄像头对空间和车位的识别,通过自动泊车系统实现全自动泊车入库。 - 功能说…

使用HHDESK图形化功能管理服务器

服务器的管理通常繁琐而枯燥&#xff0c;需要大量的命令行来执行。 所以图形化功能应运而生。 本篇以传输文件为例&#xff0c;简单介绍一下HHDESK的图形化管理功能。 首先需要配置好服务器。 点击连接管理&#xff0c;在连接类型中选择SSH&#xff0c;按照刚才在服务器中配…

方波信号轨迹跟踪(过冲与圆角)

在控制系统中&#xff0c;方波信号轨迹跟踪可能会面临过冲和圆角的问题。过冲是指跟踪信号超过期望值的现象&#xff0c;而圆角是指在方波信号变化时产生平滑的过渡。这些问题主要是因为传统的控制方法无法完美跟踪非线性的方波信号导致的。 过冲通常也称为超调。在方波信号的…

C++中的vector使用详解及重要部分底层实现

本篇文章会对vector的语法使用进行详解。同时&#xff0c;还会对重要难点部分的底层实现进行讲解。其中有vector的迭代器失效和深拷贝问题。希望本篇文章的内容会对你有所帮助。 目录 一、vector 简单概述 1、1 C语言中数组的不便 1、2 C中的动态数组容器vector 二、vector的常…

vue中实现div可编辑,并插入指定元素,样式

前言&#xff1a; vue中实现一个既可以编辑内容&#xff0c;有可以动态编辑内容插入一个带有样式的内容&#xff0c;改变默认内容后&#xff0c;这个样式消失的效果&#xff0c;这里来整理下调研与解决实现问题之路。 实现最终效果&#xff1a;图2为默认内容 1、可以光标点击任…

自定义MVC框架优化

目录 一、前言 二、优化问题 1.子控制器的初始化配置问题 2.页面跳转优化代码冗余问题 3.优化参数封装问题 三、进行优化 1.解决子控制器初始化配置 2.解决页面跳转的代码冗余问题 3.解决优化参数封装问题 4.中央控制器 一、前言 在自定义MVC框架原理中讲述了什么是…

Redis - Redis GEO实现经纬度测算距离,附近搜索范围

Redis GEO 主要用于存储地理位置信息&#xff0c;并对存储的信息进行操作&#xff0c;该功能在 Redis 3.2 版本新增 一、Redis GEO 操作方法 geoadd&#xff1a;添加地理位置的坐标 geopos&#xff1a;获取地理位置的坐标 geodist&#xff1a;计算两个位置之间的距离 geor…

client-go初级篇,从操作kubernetes到编写单元测试

欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码)&#xff1a;https://github.com/zq2599/blog_demos 本篇概览 尽管长篇系列《client-go实战》的内容足够丰富&#xff0c;然而内容太多每个知识点也有一定深度&#xff0c;对于打算快速学习并开始kubernetes开发…

分层架构简介

MVC是架构模式&#xff08;设计模式中的结构性模式&#xff09;&#xff0c;不是系统架构&#xff0c;更不是我们常说的三层架构 MVC的缺陷如下&#xff1a; 1.导致控制器冗余&#xff08;有大量的业务逻辑&#xff0c;可能开始没有&#xff0c;但是后来越来越多&#xff09;…

QT学习笔记5--槽函数重载解决办法

connect函数 connect(sender, signal, receiver, slot); 槽函数示例 void student:: treat(QString foodname) void student:: treat(int index) 由上可见&#xff0c;有两个名字相同&#xff0c;但形参不同的槽函数。 可以通过函数指针的方式 &#xff0c;用指针指向具体…

linux环境安装mysql8.0.32

linux环境安装mysql8.0.32 一、下载安装包二、安装前准备2.1 卸载旧版本mysql2.2 检查是否安装了 mariadb 数据库2.3 安装依赖包创建 mysql 用户 三、安装3.1 上传并解压安装包&#xff08;上传路径没有要求&#xff0c;一般在/usr/local&#xff09;3.2 初始化数据库3.3 注册数…

Java面试题6月

redis有哪些缓存淘汰策略 https://blog.51cto.com/u_11720620/5198874 生产环境内存溢出&#xff08;OOM&#xff09;问题处理方案 https://note.youdao.com/ynoteshare/index.html?id5cc182642eb02bc64197788c7722baae&typenote&_time1688287588653 jstack找出占用…

C++之GNU C的__attribute__((constructor))和((destructor))静态构造函数实现(一百四十八)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…

Elasticsearch实战(二十三)---ES数据建模与Mysql对比 一对多模型

Elasticsearch实战—ES数据建模与Mysql对比实现 一对多模型 文章目录 Elasticsearch实战---ES数据建模与Mysql对比实现 一对多模型1.一对多 模型1.1 Mysql建模 2.一对多 Index ES 数据模型2.1 类似Mysql, 依旧创建两个Index索引库2.2 采用ES架构 嵌套数组模型2.3采用ES架构 冗余…

【JUC-2】Synchronized关键字相关知识

Synchronized synchronized是Java中的关键字&#xff0c;是一种同步锁。它修饰的对象有以下几种&#xff1a; 修饰一个代码块&#xff0c;被修饰的代码块称为同步语句块&#xff0c;其作用的范围是大括号{}括起来的代码&#xff0c;作用的对象是调用这个代码块的对象&#xf…

【C++2】进程 信号 dbus

文章目录 1.进程&#xff1a;fork()&#xff0c;ps -ef (同-aux) | more2.信号&#xff1a;signal&#xff08;, EXIT&#xff09;&#xff0c;jps2.1 捕捉信号&#xff1a;ctrlc&#xff1a;22.2 捕捉信号&#xff1a;kill -9&#xff1a;92.3 捕捉信号&#xff1a;kill&#…

欧几里得算法

0x00 前言 改补的内容是一点都不会少。本章来看欧几里得算法 0x01 概述 欧几里得算法又称为辗转相除法&#xff0c;指用于计算两个非负整数a和b的最大公约数。 两个整数的最大公约数是能够同时整除他们的最大的正整数。 基本原理&#xff1a;两个整数的最大公约数等于其中…