通过基于注解的声明式事务实现事务功能~1

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS PostgreSQL,高可用系列 2核4GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: 通过基于注解的声明式事务实现事务功能~

编程式事务:

事务功能的相关操作全部通过自己编写代码来实现

Connection conn=.........;
try{
  //开启事务;关闭事务的自动提交
  conn.setAutoCommit(false);
  //核心操作
  //提交事务
  conn.commit;
  }
  catch(Exception e){
  //回滚事务
  conn.rollback();
}finally{
  //释放数据库连接
  conn.close();
}

上述这种编程式的实现方式存在很多缺陷:

细节没有被屏蔽,具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐
代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用


声明式事务:

既然事务控制的代码有规律可循的,并且代码的结构基本是确定的,因此框架就可以将固定模式的代码抽取出来,进行相关的封装


封装起来后,我们只需要在配置文件中进行简单的配置即可完成操作


这样做能够提高开发效率,消除了冗余的代码,框架会综合考虑相关领域中在实际开发环境下有可能遇到的各种问题,进行了健壮性,性能等各个方面的优化


基于注解的声明式事务:实现事务功能

数据库准备工作:

创建图书表:

<!-- comment是作为列或者行的注释的-->
create table t_book(book_id int not null auto_increment comment '主键',book_name varchar(20) default null comment '图书名称',price int default null comment '价格',stock int unsigned default null comment '库存(无符号)',primary key(book_id));

插入数据:

insert into t_book(book_id,book_name,price,stock)values(1,'斗破苍穹',80,100),(2,'斗罗大陆',50

创建用户表:

create table t_user(user_id int not null auto_increment comment '主键',username varchar(20) default null comment '用户名',balance int unsigned default null comment '余额(无符号)',primary key(user_id));

插入数据:

insert into t_user(user_id,username,balance) values (1,'admin',50);

通过生活经验可知,图书购买的过程往往会出现余额不足或者库存不足的情况,它就类似于我们在进行事务处理时产生的异常,因此需要执行回滚操作,但我们早在学习mysql中就说过,引起事务回滚的原因通常并不是SQL语句出现错误,而是业务的核心逻辑出现问题,但业务逻辑并不会产生异常,那么要处理出现问题的业务,我们可以在数据库层面对其进行解决,也可以在java代码中对其进行处理,在数据库中进行的处理即为,设置库存量和余额量不能小于0,具体操作为这两个字段设置关键字unsigned,设置当前字段的值是不能为负值的,当然也可以通过在java代码层面自定义异常等,当业务逻辑出现问题时,抛出异常即可

java准备工作:

添加依赖:

  <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
            <version>5.3.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.3.1</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.31</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.16</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.3.8</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.1</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>

通过创建我们之前学习的经典的三层架构模型,实现业务逻辑:

控制层:

package spring_txAnnotation.Controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import spring_txAnnotation.Service.ServiceBook;
@Controller
public class BookController {
    @Autowired
    private ServiceBook serviceBook;
    public  void  BuyBook(Integer userId,Integer BookId){
        serviceBook.buyBook(userId,BookId);
    }
}

持久层:

package spring_txAnnotation.Dao;
public interface BookDao {
    Integer getPrice(Integer bookId);//根据图书的id查询图书的价格
    void updateStock(Integer bookId);//根据图书的id修改图书的库存量
    void updateBalance(Integer userId, Integer price);//对用户的余额进行修改操作
}

持久层的实现类:

package spring_txAnnotation.Dao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class BookDaoImpl implements BookDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Override
    public Integer getPrice(Integer bookId) {
        String sql="select price from t_book where book_id=?";
        return jdbcTemplate.queryForObject(sql,Integer.class,bookId);
    }
    @Override
    public void updateStock(Integer bookId) {
        String sql="update t_book set stock=stock-1 where book_id=?";
        jdbcTemplate.update(sql,bookId);
    }
    @Override
    public void updateBalance(Integer userId,Integer price) {
        String sql="update t_user set balance=balance-? where user_id=?";
        jdbcTemplate.update(sql,price,userId);
    }
}

业务层:

package spring_txAnnotation.Service;
public interface ServiceBook  {
    void buyBook(Integer userId, Integer bookId);
}

业务层的实现类:

package spring_txAnnotation.Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import spring_txAnnotation.Dao.BookDao;
@Service
public class ServiceBookImpl implements ServiceBook {
    @Autowired
    private BookDao bookDao;
    @Override
    public void buyBook(Integer userId, Integer bookId) {
        //查询图书的价格
        Integer book_price=bookDao.getPrice(bookId);
        //更新图书的库存
        bookDao.updateStock(bookId);
        //更新用户的余额
        bookDao.updateBalance(userId,book_price);
    }
}

配置XML文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    <context:component-scan base-package="spring_txAnnotation"></context:component-scan>
    <context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
    <bean class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="datasource"></property>
    </bean>
    <bean  id="datasource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driver}"></property>
        <property name="url" value="${jdbc.url}"></property>
        <property name="username" value="${jdbc.name}"></property>
        <property name="password" value="${jdbc.password}"></property>
    </bean>
</beans>

数据库连接的部分写在外部的jdbc.properties文件中:

driver=com.mysql.cj.jdbc.Driver 
url=jdbc:mysql://localhost:3306/WJR
name=root
password=你的密码

测试类:

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import spring_txAnnotation.Controller.BookController;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:TXannotation.xml")
public class Test1 {
    @Autowired
    private BookController bookController;
    @Test
    public void testBuyBooks(){
        bookController.BuyBook(1,1);
    }
}

运行后,报错如下:

org.springframework.dao.DataIntegrityViolationException: PreparedStatementCallback; SQL [update t_user set balance=balance-? where user_id=?]; Data truncation: BIGINT UNSIGNED value is out of range in '(`wjr`.`t_user`.`balance` - 80)'; nested exception is com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: BIGINT UNSIGNED value is out of range in '(`wjr`.`t_user`.`balance` - 80)'

原因如下:属性为无符号的这个字段的值超出了范围

Caused by: com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: BIGINT UNSIGNED value is out of range in '(`wjr`.`t_user`.`balance` - 80)'

上述的SQL语句对数据实现的是,用当前余额的50元去购买价格为80的图书编号为1的图书,二者相减后的结果为负数,但因为我们将其类型设置为无符号的类型,因此就产生错误


在之前学习mysql中,我们就讲到mysql默认是有提交事务的功能,一个SQL语句对应一个事务提交,那么自动提交事务的功能会带来什么麻烦呢?


如下所示:


下述的这三个方法各自对应单独的事务,也就是说,他们事务提交成功与否只和自身有关,与其他操作并没有关系

首先我们先来查看一下数据库中的数据情况,如下所示:

当我们发生了使用不足的余额去购买图书这个操作后,java控制台报错如上述,数据库中的数据变化,如下所示:

我们发现用户的余额并没有任何的改变,但是1号图书的库存量减少了1,那么也证实了我们上述所说的,不同的操作之间是互不影响的,但这种结果,显然不符合逻辑,下面我们就通过基于注解的声明式事务来对这种情况进行处理


上面在学习声明式事务的概念时,我们提到过,声明式事务不需要我们单独的写切面和通知,只需要在配置文件中进行简单的配置即可完成操作


第一步:在XML文件中,配置事务管理器-->处理数据必须有数据源,如下所示:


由于事务管理器是一个接口,如果想将接口设置为bean,则必须通过其实现类来完成

<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
        <!--引用数据源-->
        <property name="dataSource" ref="dataSource"></property>
</bean>

第二步:在XML文件中,开启事务的注解驱动–>驱动并不是环绕通知,事务管理器才是,我们通过注解驱动中的transaction-manager属性将其二者关联起来 ,而属性值使用为默认值

<tx:annotation-driven transaction-manager="transactionManager"/>

注意:这里的annotation有多个,一定要使用tx的

第三步:将其注解使用在方法上

使用@Transactional注解所标识的方法或类中所有的方法使用事务进行管理,也就是说@Transactional作用于那个方法上,那个方法就是连接点,如果加到类上,则该类上的所有方法都是连接点,transaction-manager用来设置事务管理器的id,如果该id是默认值[transactionManager],可以省略不写


如下所示:将其添加至方法上

报错如下:

org.springframework.dao.DataIntegrityViolationException: PreparedStatementCallback; SQL [update t_user set balance=balance-? where user_id=?]; Data truncation: BIGINT UNSIGNED value is out of range in '(`wjr`.`t_user`.`balance` - 80)'; nested exception is com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: BIGINT UNSIGNED value is out of range in '(`wjr`.`t_user`.`balance` - 80)'
Caused by: com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: BIGINT UNSIGNED value is out of range in '(`wjr`.`t_user`.`balance` - 80)'

咿?不禁让人产生疑惑,怎么进行事务管理之后的报错内容和上述没有进行管理时完全相同啊,好像事务管理的作用并没有在这里体现,那么我们再去查看数据库部分,是否也和上面完全相同呢?


如下所示:


与上述未进行事务管理不同的地方在于,这里的1号图书的数量并没有减少,用户余额同样也是,因为余额不足的情况下,购买失败,因此整个事务进行了回滚,显然这种才是符合逻辑的

声明式事务的属性:只读,超时,回滚策略

事务属性:只读

对一个查询操作来说如果我们把它设置为只读,就能明确的告诉数据库,这个操作不涉及写操作,这样数据库就能够针对查询操作来进行优化

设置方法:

@Transactional(readOnly = true)

大家注意看该属性值的默认值为false,那么也就是说,只有在事务中的操作都为查询操作时,才需要将其设置为只读

注意:只有当事务中的操作都为查询操作时,才能设置事务的只读操作,而如果事务中出现了其他的操作,我们依然将其设置为只读,就会报错

举例:当我们在包含除查询以外的其他操作的事务中,将该事务设置为只读:

报错如下所示:

事务属性:超时

事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源,而长时间占用资源,大概率是因为程序运行出现了问题(可能是java程序或者mysql数据库或网络连接等等)


此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常程序可以执行


概括来说就是一句话:超时回滚,释放资源

设置方法:

//将当前的事务执行时间设置为3秒钟,如果超过三秒钟,则直接抛出异常
@Transactional(timeout = 3)


举例:

报错如下:事务超时异常

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。 &nbsp; 相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/mysql&nbsp;
相关文章
|
小程序 前端开发 程序员
不得不说,这19个程序员兼职平台让我1年收入60w
关于程序员接私活,社会各界说法不一。
1978 1
|
存储 Java 测试技术
【Java】剑指offer(32)从上往下打印二叉树
【Java】剑指offer(32)从上往下打印二叉树
|
应用服务中间件 Linux 网络安全
CentOS 7 上安装 Nginx
在 CentOS 7 上安装 Nginx 的步骤包括:添加 EPEL 仓库,安装 Nginx,启动 Nginx,配置防火墙规则,最后通过访问服务器 IP 验证安装是否成功
625 0
|
12月前
|
数据采集 存储 监控
Java爬虫:数据采集的强大工具
在数据驱动的时代,Java爬虫技术凭借其强大的功能和灵活性,成为企业获取市场信息、用户行为及竞争情报的关键工具。本文详细介绍了Java爬虫的工作原理、应用场景、构建方法及其重要性,强调了在合法合规的前提下,如何有效利用Java爬虫技术为企业决策提供支持。
HarmonyOS ArkTS Ability内页面的跳转和数据传递
HarmonyOS ArkTS Ability 的数据传递包括有 Ability 内页面的跳转和数据传递、Ability 间的数据跳转和数据传递。本节主要讲解 Ability 内页面的跳转和数据传递。 打开 DevEco Studio,选择一个 Empty Ability 工程模板,创建一个名为 “ArkUIPagesRouter” 的工程为演示示例。
1175 1
|
存储 前端开发 Linux
Windows 安装 Podman Desktop
Podman(POD MANager) 是一个用于管理容器和映像、挂载到这些容器中的卷以及由容器组组成的 pod 的工具。Podman 在 Linux 上运行容器,但也可以使用 Podman 管理的虚拟机在 Mac 和 Windows 系统上使用。 Podman 基于 libpod,libpod 是一个用于容器生命周期管理的库,也包含在此存储库中。libpod 库提供了用于管理 containers(容器)、pods、 container images(容器镜像)和 volumes(卷)的 API。
2322 0
如何对SNP设计引物: CAPS, dCAPS
最近一直在帮师姐根据SNP找基因组上的酶切多态位点,然后给她提供该位点附近1kb的序列,让她去设计引物。由于我本科就做过一点点的遗传定位,当时用的是SSCP(单分子构象多态性)去区分单个碱基差异,所以我对SNP分子比较的检测方法就局限在高通量测序和SSCP而已。
2951 0
|
新零售 C++
竞品分析 VS 功能分析
前面的文中有写过一篇竞品部分功能分析的文章,今天来聊下一整个产品如何分析,并对二者进行简单的比较。小伙伴们不要太在意概念,其实都是竞品分析,可以这样理解,一个分析的是面,一个分析的是点,主要还是以目的为导向,如果你只是为了优化某一个功能模块而参考竞品,那就没必要做个大而全的产品分析,如果你操盘一个产品那就有必要做整个产品的分析了。
1817 0
|
Linux Docker 容器
Docker企业版安装指南
本文介绍了Docker企业版的安装步骤,包含了Docker UCP和Docker DTR的安装命令以及页面配置。
3243 0
下一篇
开通oss服务