Spring Boot中的事务管理与手把手实战
项目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 = InnoDBDEFAULT CHARSET = utf8;create table court(
`court_id` INT NOT NULL,
`num` INT NOT NULL DEFAULT 0,
PRIMARY KEY(`court_id`)
)ENGINE = InnoDBDEFAULT 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)@NotNullprivate int id;@NotNullprivate int courtId;@NotNullprivate 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@NotNullprivate int courtId;@NotNullprivate 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;@Autowiredprivate UserDao userDao;@Autowiredprivate 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 {@Autowiredprivate 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")@ResponseBodypublic 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 causejava.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注解:
@Transactionalpublic 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表也没有新插入数据,达到了我们进行事务控制的目的。
Spring Boot中的事务管理与手把手实战相关推荐
- Spring Boot中的事务管理
什么是事务? 我们在开发企业应用时,对于业务人员的一个操作实际是对数据读写的多步操作的结合.由于数据操作在顺序执行的过程中,任何一步操作都有可能发生异常,异常会导致后续操作无法完成,此时由于业务逻辑并 ...
- twilio_15分钟内使用Twilio和Stormpath在Spring Boot中进行身份管理
twilio 建筑物身份管理,包括身份验证和授权? 尝试Stormpath! 我们的REST API和强大的Java SDK支持可以消除您的安全风险,并且可以在几分钟内实现. 注册 ,再也不会建立au ...
- 15分钟内使用Twilio和Stormpath在Spring Boot中进行身份管理
建筑物身份管理,包括身份验证和授权? 尝试Stormpath! 我们的REST API和强大的Java SDK支持可以消除您的安全风险,并且可以在几分钟内实现. 注册 ,再也不会建立auth了! 今天 ...
- Spring Boot 声明式事务 @Transactional 的使用
1.Spring Boot 项目中使用事务 首先使用 @EnableTransactionManagement 注解开启事务支持,然后在需要事务管理的 public 方法上添加注解 @Transact ...
- Spring中的事务管理详解
在这里主要介绍Spring对事务管理的一些理论知识,实战方面参考上一篇博文: http://www.cnblogs.com/longshiyVip/p/5061547.html 1. 事务简介: 事务 ...
- springboot mysql事物_在Spring Boot中使用数据库事务
关于数据库访问还有一个核心操作那就是事务的处理了,前面两篇博客小伙伴们已经见识到Spring Boot带给我们的巨大便利了,其实不用猜,我们也知道Spring Boot在数据库事务处理问题上也给我们带 ...
- spring事务管理器的作用_【面试必问】Spring中的事务管理详解
在这里主要介绍Spring对事务管理的一些理论知识,实战方面参考上一篇博文: http://www.cnblogs.com/longshiyVip/p/5061547.html 1. 事务简介: 事务 ...
- java事务是基于数据库的么_详解在Spring Boot中使用数据库事务
我们在前面已经分别介绍了如何在spring Boot中使用JPA以及如何在Spring Boot中输出REST资源.那么关于数据库访问还有一个核心操作那就是事务的处理了,前面两篇博客小伙伴们已经见识到 ...
- Spring Boot中的多事务管理
** 一.多数据源问题 ** 最后一个可以插入一条数据,原因是Transactoinal的事务只针对userMapper有效,因为之前的事务只给test1配置了,而test2并没有配置事务. ** 二 ...
- Spring Boot中使用LDAP来统一管理用户信息
很多时候,我们在构建系统的时候都会自己创建用户管理体系,这对于开发人员来说并不是什么难事,但是当我们需要维护多个不同系统并且相同用户跨系统使用的情况下,如果每个系统维护自己的用户信息,那么此时用户信息 ...
最新文章
- PyCharm入门教程——在编辑器中使用拖放
- NYOJ 45 棋盘覆盖
- 25个提高网站可用性和转化率的工具
- 第五篇: mysqlduomp 实时增量备份、innobackupex(完全备份与 恢复、增量备份与恢复)
- SpringBoot 上传多个文件
- Update operation on extension field created by AET
- python函数的作用复用代码_Python-函数和代码复用
- 将2个字符写入单个Java char
- spring集成mybatis后怎么在控制台打印sql语句
- 第二章 吸取jQuery之选择器和包装集
- mysql group by 分组查询
- Go Module 私有仓库:fatal: could not read Username for ‘https://xxx.com‘: terminal prompts disabled
- 关于数据分析岗位的工作思考
- [原创]WIA 学习笔记
- 深度参与 openGauss Developer Day 2022,云和恩墨在多项活动中展风采
- 【文智背后的奥秘】系列篇——结构化抽取平台
- 怎么正确使用代理IP
- 联发科(MTK)MT6765 核心板 安卓主板
- python 空数组判断
- SpringBoot入门教程 Lombok使用注意事项