二、简单事务使用 模拟一
1、数据库
db.sql
drop database if exists supermarket;
create database supermarket;
use supermarket;
drop table if exists `order_item`;
CREATE TABLE `order_item` (
`item_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '订单ID',
`order_no` varchar(64) NOT NULL COMMENT '订单号',
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`goods_id` bigint(20) NOT NULL COMMENT '商品ID',
`price` decimal(12,2) NOT NULL COMMENT '单价',
`count` int(11) NOT NULL COMMENT '数量',
PRIMARY KEY (`item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 comment '订单项表';
drop table if exists `good_stock` ;
CREATE TABLE `good_stock` (
`stock_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '库存ID',
`goods_id` bigint(20) NOT NULL COMMENT '商品ID',
`total` int(11) NOT NULL COMMENT '总数量',
`sold` int(11) NOT NULL COMMENT '已售出',
PRIMARY KEY (`stock_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 comment '商品库存表';
insert into good_stock (goods_id,total,sold) values(100000,10,0);
student.sql
DROP TABLE IF EXISTS `student`;
CREATE TABLE `student` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(10) NOT NULL,
`sex` varchar(255) DEFAULT '',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1004 DEFAULT CHARSET=utf8;
INSERT INTO `student` VALUES (1001,'张三','boy'),(1002,'李四','girl'),(1003,'王五','secret'),(1004,'hehe','female'),(1005,'hehe','female'),(1006,'hehe','female'),(1007,'hehe','female'),(1008,'hehe','female'),(1009,'hehe','female');
2、pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.study</groupId>
<artifactId>springboot-transactional</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-transactional</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
</configuration>
</plugin>
</plugins>
</build>
</project>
2、实体类
src/main/java/com/study/springboottransactional/entity/Student.java
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Student {
private int id;
private String name;
private String sex;
}
3、配置信息
src/main/resources/application.properties
#mysql的配置信息
spring.datasource.url=jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#支持SQL 输出
spring.jpa.show-sql=true
#format 一下 SQL 进行输出
spring.jpa.properties.hibernate.format_sql=true
#自动生成开启,让表数据会自动跟随entity类的变化而变化
spring.jpa.hibernate.ddl-auto=update
#开启自动驼峰命名规则(camel case)映射,即从经典数据库列名 A_COLUMN 到经典 Java 属性名 aColumn 的类似映射。
mybatis.configuration.mapUnderscoreToCamelCase=true
4、接口
src/main/java/com/study/springboottransactional/repository/StudentRespository.java
@Repository
public interface StudentRespository {
@Update(value = "insert into student(name,sex) " +
"values(#{student.name},#{student.sex})")
void insertStudent(@Param("student") Student student);
@Select(value = "select * from student where id=(select @@IDENTITY)")
Student findStudentByNear();
}
src/main/java/com/study/springboottransactional/service/StudentService.java
@Service
public class StudentService {
@Autowired
private StudentRespository studentRespository;
public Student addStudent(String name,String sex){
Student stu = Student.builder().name(name).sex(sex).build();
studentRespository.insertStudent(stu);
//模拟在数据库操作完成之后发生一个异常操作
// int i = 1/0;
return studentRespository.findStudentByNear();
}
}
src/main/java/com/study/springboottransactional/controller/StudentController.java
@RestController //直接返回数据
public class StudentController {
@Autowired
private StudentService studentService;
@RequestMapping("stu/add")
public Object addStudent(@Param("name") String name, @Param("sex") String sex){
return studentService.addStudent(name, sex);
}
}
src/main/java/com/study/springboottransactional/SpringbootTransactionalApplication.java
@MapperScan("com.study.springboottransactional.repository")
@SpringBootApplication
public class SpringbootTransactionalApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootTransactionalApplication.class, args);
}
}
5、未加入异常执行结果
127.0.0.1:8080/stu/add?name=lisi&sex= male
5、加入异常
src/main/java/com/study/springboottransactional/service/StudentService.java
@Service
public class StudentService {
@Autowired
private StudentRespository studentRespository;
public Student addStudent(String name,String sex){
Student stu = Student.builder().name(name).sex(sex).build();
studentRespository.insertStudent(stu);
//模拟在数据库操作完成之后发生一个异常操作
int i = 1/0;//加入 的异常
return studentRespository.findStudentByNear();
}
}
127.0.0.1:8080/stu/add?name=wangwu&sex= male
查看一下数据库wangwu 是否插入数据库,发现插入了,就有问题
6、解决
加入@Transactional
注解
@Service
@Transactional
public class StudentService {
@Autowired
private StudentRespository studentRespository;
public Student addStudent(String name,String sex){
Student stu = Student.builder().name(name).sex(sex).build();
studentRespository.insertStudent(stu);
//模拟在数据库操作完成之后发生一个异常操作
// int i = 1/0;
return studentRespository.findStudentByNear();
}
}
删除wang五这条数据再次测试
127.0.0.1:8080/stu/add?name=wangwu&sex= male
这次数据库中没有插入
三、@Transactional 注解讲解
1、@Transactional的属性
1.1、readOnly 只读
该属性用于设置当前事务是否为只读事务,设置为true表示只读,false则表示可读写, 默认值为false。 例如:
@Transactional(readOnly=true)
使用这个,就无法增删改,只能读取
@Service
@Transactional
public class StudentService {
@Autowired
private StudentRespository studentRespository;
@Transactional(readOnly = true)
public Student addStudent(String name,String sex){
Student stu = Student.builder().name(name).sex(sex).build();
studentRespository.insertStudent(stu);
return studentRespository.findStudentByNear();
}
}
127.0.0.1:8080/stu/add?name=wangwu&sex= male 报错
127.0.0.1:8080/stu/findAl
1.2、rollbackFor 和 rollbackForClassName
出现某些异常,进行回滚
一般用rollbackFor
出现某些异常,不进行回滚
一般用noRollbackFor
回滚是有默认值的,runtimeexception、和error
1.3、propagation
绝大部分都可以用这个事务
儿子干完 老爸再干活
依靠外人活着
1.4、timeout
1.5、isolation
四、@Transactional中的属性propagation讲解
➢@Transactional(propagation=Propagation.REQUIRED) :如果有事务,那么加入事务,没有的话新建一个(默认情况
下)
➢@Transactional(propagation=Propagation.NOT_ SUPPORTED) :容器不为这个方法开启事务
➢@Transactional(propagation=Propagation.REQUIRES_ NEW):不管是否存在事务,都创建一个新的事 务,原来的挂
起新的执行完毕,继续执行老的事务
➢@Transactional(propagation=Propagation.MANDATORY): 必须在-一个已有的事务中执行,否则抛出异常
➢@Transactional(propagation=Propagation.NEVER) :必须在一个没有 的事务中执行,否则抛出异常(与
Propagation.MANDATORY相反)
➢@Transactional(propagation=Propagation.SUPPORTS) :如果其他bean调用这个方法,在其他bean中声明事务,那
就用事务.如果其他bean没有声明事务,那就不用事务.
1、propagation.REQUIRED
@Transactional(propagation=Propagation.REQUIRED):
-
当A方法调用B方法的时候
-
如果A方法没有开启事务,因为此时B方法开启了事务,A方法就会默认开启事务
-
如果运行的时候B方法发生异常回滚,A方法也会发生回滚
-
平时使用使用最多的
-
2、propagation.REQUIRES_NEW
@Transactional(propagation=Propagation.REQUIRES_ NEW):
不管是否存在事务,都创建一个新的事务 ,原来的挂起,新的执行完毕,继续执行老的事务
如果案例中的改成REQUIRES_NEW
,如果B方法产生异常回滚,如果A方法没有捕捉处理掉异常,A方法也会回滚
3、propagation. MANDATORY
@Transactional(propagation=Propagation.MANDATORY):
必须在一个已有的事务中执行,否则抛出异常
4、其他属性使用很少
五、@Transactional中的属性propagation属性timeout
5.1、介绍的demo
➢设置事务的超时时间,单位秒:
➢属性解释:
当某个业务运行的时间超过你的预期时,可以使用
该属性来让该业务抛出异常并且强制回滚;
5.2、使用场景
比如银行业务中,当用户进行取现出票失败时,
可以使用该属性来强制结束业务,进行回滚,减少用户等待的时间。
好处是能告诉你 失败,不会让客户一直刷新界面
5.3、demo
六、propagation属性isolation
一般使用可重复读
七、实战中的应用
1、场景描述
在商城项目中,往往会存在这两张表:订单表和库存表,那么这两张表在很多业务中都是同时成功或失败的,比
如用户下单,退货等操作,所以我们需要将这两个操作放在一个事务中进行控制。
2、数据库
drop database if exists supermarket;
create database supermarket;
use supermarket;
drop table if exists `order_item`;
CREATE TABLE `order_item` (
`item_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '订单ID',
`order_no` varchar(64) NOT NULL COMMENT '订单号',
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`goods_id` bigint(20) NOT NULL COMMENT '商品ID',
`price` decimal(12,2) NOT NULL COMMENT '单价',
`count` int(11) NOT NULL COMMENT '数量',
PRIMARY KEY (`item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 comment '订单项表';
drop table if exists `good_stock` ;
CREATE TABLE `good_stock` (
`stock_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '库存ID',
`goods_id` bigint(20) NOT NULL COMMENT '商品ID',
`total` int(11) NOT NULL COMMENT '总数量',
`sold` int(11) NOT NULL COMMENT '已售出',
PRIMARY KEY (`stock_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 comment '商品库存表';
insert into good_stock (goods_id,total,sold) values(100000,10,0);
3、实体类
src/main/java/com/study/springboottransactional/entity/OrderItem.java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderItem {
//订单ID
private Long itemId;
//订单号
private String orderNo;
//用户ID
private Long userId;
//商品ID
private Long goodsId;
//单价
private BigDecimal price;
//数量
private Integer count;
}
src/main/java/com/study/springboottransactional/entity/GoodStock.java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class GoodStock {
//库存ID
private Long stockId;
//商品ID
private Long goodsId;
//总数量
private Integer total;
//已售出
private Integer sold;
}
src/main/java/com/study/springboottransactional/repository/GoodStockRepository.java
@Repository
public interface GoodStockRepository{
@Update("update good_stock set sold=sold+#{reduceCount} where goods_id =#{goodsId} and total>=sold+#{reduceCount};")
int reduceStock(@Param("goodsId") long goodsId, @Param("reduceCount") int reduceCount);
}
src/main/java/com/study/springboottransactional/repository/OrderItemRepository.java
SpringBoot @Repository解析 - 简书 (jianshu.com)
@Repository
public interface OrderItemRepository{
@Update(value = "insert into order_item(order_no,user_id,goods_id,price,count) " +
"values(#{orderItem.orderNo},#{orderItem.userId},#{orderItem.goodsId},#{orderItem.price},#{orderItem.count})")
void insertOrderItem(@Param("orderItem") OrderItem orderItem);
@Select(value = "select * from order_item where item_id=(select @@IDENTITY)")
// @Results({
// @Result(property = "itemId", column = "item_id"),
// @Result(property = "orderNo", column = "order_no"),
// @Result(property = "userId",column = "user_id"),
// @Result(property = "goodsId",column = "goods_id")
// })
OrderItem findOrderItemByNear();
}
src/main/java/com/study/springboottransactional/service/OrderService.java
@Service
public class OrderService {
@Autowired
private OrderItemRepository orderItemRepository;
@Autowired
private GoodStockRepository goodStockRepository;
public OrderItem addOrder(long goodsId, long userId, int count){
//创建订单
OrderItem orderItem = OrderItem.builder().orderNo(UUID.randomUUID().toString().replace("-", "")).
price(new BigDecimal(goodsId)).userId(userId).count(count).goodsId(goodsId).build();
orderItemRepository.insertOrderItem(orderItem);
//扣减库存
int updateRows = goodStockRepository.reduceStock(goodsId, count);
if(updateRows <= 0){
throw new IllegalArgumentException("下单失败,库存不足!");
}
// if(true) {
// throw new RuntimeException("手动抛出异常,但是不回滚");
// }
OrderItem orderItemByNear = orderItemRepository.findOrderItemByNear();
return orderItemByNear;
}
}
127.0.0.1:8080/add/order?goodsld= 00000&count= 1
库存表的数量是10,但是却卖了11件 ,total为库存数量
以下是卖出的数量
@Service
public class OrderService {
@Autowired
private OrderItemRepository orderItemRepository;
@Autowired
private GoodStockRepository goodStockRepository;
@Transactional
public OrderItem addOrder(long goodsId, long userId, int count){
//创建订单
OrderItem orderItem = OrderItem.builder().orderNo(UUID.randomUUID().toString().replace("-", "")).
price(new BigDecimal(goodsId)).userId(userId).count(count).goodsId(goodsId).build();
orderItemRepository.insertOrderItem(orderItem);
//扣减库存
int updateRows = goodStockRepository.reduceStock(goodsId, count);
if(updateRows <= 0){
throw new IllegalArgumentException("下单失败,库存不足!");
}
// if(true) {
// throw new RuntimeException("手动抛出异常,但是不回滚");
// }
OrderItem orderItemByNear = orderItemRepository.findOrderItemByNear();
return orderItemByNear;
}
}
可以@Transactional(noRollbackFor = RuntimeException.class)加上这个不回滚