Spring Boot中的事务管理与手把手实战

本文详细介绍了一个基于Spring框架的事务管理实战案例,通过一个具体的报名场景,演示了如何使用@Transactional注解来确保数据库操作的原子性和一致性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

项目github地址:bitcarmanlee easy-algorithm-interview-and-practice
欢迎大家star,留言,一起学习进步

1.数据库中的事务

如果一个数据库声称支持事务的操作,那么该数据库必须要具备以下四个特性:
1.原子性(Atomicity)
原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,这和前面两篇博客介绍事务的功能是一样的概念,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。

2.一致性(Consistency)
一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。
拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。

3.隔离性(Isolation)
隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。

4.持久性(Durability)
持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。

2.事务的特点

数据库事务的特点,用一个最简单的词来形容,则是"同生共死"。
事务指访问并可能更新数据库中各种数据项的一个程序执行单元。一般来说,都是由多个sql语句组成,并且作为一个整体执行。
以经典的账户转账为例,如果要将用户A的钱转给用户B,至少需要两部:
1.A账户中的资金减少。
2.B账户中的资金增加。
那么转账过程中,需要这两个步骤都同时执行成功,转账过程才能执行。

事务的一般语法步骤为:
开始事务:BEGIN TRANSACTION 开启事务
事务提交:COMMIT TRANSACTION --提交操作
事务回滚:ROLLBACK TRANSACTION --取消操作

3.Spring中的事务管理

Spring 事务管理分为编码式和声明式的两种方式。编程式事务指的是通过编码方式实现事务;声明式事务基于 AOP,将具体业务逻辑与事务处理解耦。声明式事务管理使业务代码逻辑不受污染, 因此在实际使用中声明式事务用的比较多。声明式事务有两种方式,一种是在配置文件(xml)中做相关的事务规则声明,另一种是基于@Transactional 注解的方式。@Transactional注解的使用方式非常简单明了,所以我们来实验一下这种方式。

4.Spring事务管理实战

上面说了这么多理论,肯定不是我们的风格,下面我们以一个实际的场景来说明Spring中怎么用注解的方式来处理事务。
假设我们有个打球报名的需求,具体的需求如下:
1.用户以某个userId报名参加某个场地courtId的打球活动。
2.某个场地courtId的报名人数不能超过一定的总量。
当然实际的需求比这个更发杂,我们先把问题简单化,假设先只要满足上述的条件。

4.1 在mysql中创建两张表:user与court

假设本机已经装好了mysql数据库的server并已经成功启动mysql服务。

create table user(
`id` INT NOT NULL AUTO_INCREMENT,
`court_id` INT NOT NULL DEFAULT 1,
`user_id` INT NOT NULL DEFAULT 0,
PRIMARY KEY(`id`)
)
  ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

create table court(
`court_id` INT NOT NULL,
`num` INT NOT NULL DEFAULT 0,
PRIMARY KEY(`court_id`)
)
  ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

其中user表存用户的报名信息,id为自增主键,某个用户报一次名就往该表插入一条数据。
而court表存储的是某个courtId的报名人数。

2.项目中的pom依赖

新建一个maven web项目,其中pom.xml将spring jpa相关的依赖引入:

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>6.0.2.Final</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.3.2</version>
        </dependency>
    </dependencies>

简单起见,只将dependency部分贴出来,分别是mysql, spring, hibernate的一些配置

3.将mysql表对应到model层

user表对应的model:

import javax.persistence.*;
import javax.validation.constraints.NotNull;

/**
 * Created by WangLei on 18-6-4.
 */
@Entity
@Table(name = "user")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @NotNull
    private int id;

    @NotNull
    private int courtId;

    @NotNull
    private int userId;

    public User() {}

    public User(int courtId, int userId) {
        this.courtId = courtId;
        this.userId = userId;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getCourtId() {
        return courtId;
    }

    public void setCourtId(int courtId) {
        this.courtId = courtId;
    }

    public int getUserId() {
        return userId;
    }

    public void setUserId(int userId) {
        this.userId = userId;
    }
}

court表对应的model:

import javax.persistence.*;
import javax.validation.constraints.NotNull;

/**
 * Created by WangLei on 18-6-4.
 */
@Entity
@Table(name = "court")
public class Court {

    @Id
    @NotNull
    private int courtId;

    @NotNull
    private int num;

    public Court() {
    }

    public Court(int courtId, int num) {
        this.courtId = courtId;
        this.num = num;
    }

    public int getCourtId() {
        return courtId;
    }

    public void setCourtId(int courtId) {
        this.courtId = courtId;
    }

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }
}

顺便解释一下Spring中的这几个注解:
@Entity:对实体注释。任何Hibernate映射对象都要有这个注释
@Table: 声明此对象映射到数据库的数据表,通过它可以为实体指定表(talbe),目录(Catalog)和schema的名字。该注释不是必须的,如果没有则系统使用默认值(实体的短类名)。不过一般我还是习惯指定表名,这样出错的可能性更小。
@Id:声明此属性为主键。该属性值可以通过应该自身创建,但是Hibernate推荐通过Hibernate生成。

4.dao层(spring中也叫Repository,不过咱们还是叫dao)

user对应的dao

import com.xiaomi.xxx.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

/**
 * Created by WangLei on 18-6-4.
 */
@Transactional
@Repository
public interface UserDao  extends JpaRepository<User, Integer> {

}

court对应的dao:

import com.xiaomi.xxx.model.Court;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

/**
 * Created by WangLei on 18-6-4.
 */
@Transactional
@Repository
public interface CourtDao extends JpaRepository<Court, Integer> {

    @Query(value = "select c from Court c where c.courtId = ?1")
    Court findByCourtId(int courtId);

    @Modifying
    @Query(value = "update Court c set c.num = c.num + 1 where c.courtId = ?1")
    int updateCourt(int courtId);
}

注意dao类都继承了JpaRepository,JpaRepository里面就封装了数据库的CRUD操作。

5.service层

将更新操作的业务逻辑封装到service层中

import com.xiaomi.xxx.dao.CourtDao;
import com.xiaomi.xxx.dao.UserDao;
import com.xiaomi.xxx.model.Court;
import com.xiaomi.xxx.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * Created by WangLei on 18-6-4.
 */
@Service
public class UserService {

    // 每个场地对多10人
    private int COURT_MAX_NUM = 10;

    @Autowired
    private UserDao userDao;

    @Autowired
    private CourtDao courtDao;

    public String addUser(int courtId, int userId) {
        Court court = courtDao.findByCourtId(courtId);

        if(court.getNum() == COURT_MAX_NUM) {
            return "场地报名超过限制!";
        }

        if(court == null) {
            courtDao.save(new Court(courtId, 1));
        } else {
            courtDao.updateCourt(courtId);
        }

        User result = userDao.save(new User(courtId, userId));
        return "add user " + result.getUserId() + " success!";
    }

}

6.contoller层

import com.xiaomi.xxx.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * Created by WangLei on 18-6-4.
 */
@RestController
public class UserController {

    @Autowired
    private UserService userService;

    @RequestMapping(value = "/addUser")
    public String addUser(int courtId, int userId) {
        return userService.addUser(courtId, userId);
    }

}

7.Appliction入口

整个项目的入口类:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * Created by WangLei on 18-5-29.
 */
@SpringBootApplication
@Controller
public class Application {

    @RequestMapping("/hello")
    @ResponseBody
    public String index() {
        return "hello world!";
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

8.将整个项目run起来

在intellij点击开始按钮,将整个项目启动

...
2018-06-04 18:31:56.320  INFO 20697 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2018-06-04 18:31:56.320  INFO 20697 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2018-06-04 18:31:56.336  INFO 20697 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-04 18:31:56.336  INFO 20697 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-04 18:31:56.354  INFO 20697 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-04 18:31:56.472  INFO 20697 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2018-06-04 18:31:56.537  INFO 20697 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2018-06-04 18:31:56.540  INFO 20697 --- [           main] com.xiaomi.xxx.Application               : Started Application in 7.032 seconds (JVM running for 10.144)

这个时候说明整个项目已经启动成功!

5.添加用户

项目成功run起来以后,我们开始在浏览器中发起http请求:

http://localhost:8080/addUser?courtId=1&userId=1000

点击确认,这个时候浏览器会返回如下结果:

add user 1000 success!

在去我们本地数据库中查看一下数据库中表的结果:

mysql> select * from court;
+----------+-----+
| court_id | num |
+----------+-----+
|        1 |   1 |
+----------+-----+
1 row in set (0.00 sec)

mysql> select * from user;
+----+----------+---------+
| id | court_id | user_id |
+----+----------+---------+
|  1 |        1 |    1000 |
+----+----------+---------+
1 row in set (0.00 sec)

此时user表中已经插入一条数据,表示user_id为1000的用户报名了1号场活动,而1号场已经有一个人报名,符合我们的预期!

6.上面的代码没有事务管理

上面的代码存在有一个很严重的问题,那就是service里的addUser方法,需要执行两条sql:

        if(court == null) {
            courtDao.save(new Court(courtId, 1));
        } else {
            courtDao.updateCourt(courtId);
        }

        User result = userDao.save(new User(courtId, userId));

这两个sql针对两个表执行:
1.先对court表进行加1的操作。
2.再对user表进行插入一条数据的操作。
很明显,这两条sql就是一个典型的事务。如果第一条sql执行成功,而第二条sql执行失败,此时会对court表进行加1的操作。很明显,这跟我们的业务需求是完全不吻合的。此时我们需要做的,是对第一条sql进行回滚操作,保证整个事务没有执行成功。

我们现在来模拟一下第一条sql执行,然后执行到第二条sql出现异常的情况。

首先将service部分的代码稍作修改:

        if(court == null) {
            courtDao.save(new Court(courtId, 1));
        } else {
            courtDao.updateCourt(courtId);
        }

        int div = 1 / 0;
        User result = userDao.save(new User(courtId, userId));
        return "add user " + result.getUserId() + " success!";

div那行,0不能当除数,所以代码运行到这一行会有异常退出。

重新将服务起来,然后在浏览器中继续发起一个请求:

http://localhost:8080/addUser?courtId=1&userId=1001

这个时候IDE里会爆出如下异常:

2018-06-05 09:37:31.063 ERROR 31697 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause

java.lang.ArithmeticException: / by zero
...

此时我们再查一下数据库:

mysql> select * from court;
+----------+-----+
| court_id | num |
+----------+-----+
|        1 |   2 |
+----------+-----+
1 row in set (0.00 sec)

mysql> select * from user;
+----+----------+---------+
| id | court_id | user_id |
+----+----------+---------+
|  1 |        1 |    1000 |
+----+----------+---------+
1 row in set (0.00 sec)

此时court表已经进行了update操作,而user表没有插入新的数据行!

7.在Spring中使用Transactional来进行事务管理

那么我们怎么进行事务管理呢?根据第二部分的描述,最简单的方式是使用@Transactional注解的方式。
在addUser方法前面加上@Transactional注解:

    @Transactional
    public String addUser(int courtId, int userId)

然后重新启动服务,在浏览器中再发起一次请求:

http://localhost:8080/addUser?courtId=1&userId=1001

会报同样的除数为0的异常。
再去数据库里看一把:

mysql> select * from court;
+----------+-----+
| court_id | num |
+----------+-----+
|        1 |   2 |
+----------+-----+
1 row in set (0.00 sec)

mysql> select * from user;
+----+----------+---------+
| id | court_id | user_id |
+----+----------+---------+
|  1 |        1 |    1000 |
+----+----------+---------+
1 row in set (0.00 sec)

很明显,此时court表没有进行update操作,user表也没有新插入数据,达到了我们进行事务控制的目的。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值