SpringBoot手写事务
接着上一篇手写ORM框架之后,我们再手写一个spring事务,来帮助我们了解一下事务!
为了我们更好的了解本文,首先看一下我们项目的前身 手写ORM框架
通常我们事务都采用spring的事务,那么如果我们不使用spring的事务,我们应该如何控制我们代码中的数据一致性呢!
问题:我们执行数据库操作,一个方法中既有操作A表的新增,又有操作B表的新增,如何保证事务一致呢
通常我们执行A表操作时,获取数据库连接,执行操作,关闭连接,然后调用B表保存方法,会继续获取一个连接,执行操作,关闭连接,那么我们是不是无法保证这两个操作同时成功或者同事失败,因为他们两个的connection是两个!
思路:如果我们刚进入这个方法,首先获取一个数据库连接Connection,通过这个connection执行A表的操作,又执行B表的操作,最后提交事务,关闭connection,是不是就OK了呀!
有了这个之后我们会立马想到什么?不错就是spring的核心之一AOP,执行操作前和执行操作后一系列操作,就可以用AOP来解决。我们立马会在service调用mapper之前加入AOP,但是这样是不是每个写业务代码的人都要手写一段这样的代码,违背了开闭原则中的对修改关闭,对扩展打开。我们如何做到不对业务代码无侵入式呢!
在这里很多同学估计会想到答案,用注解,不错,spring不也是这样吗!在service类中的新增,删除和修改的方法上面加入@Transction
,加完之后不用理会其他,继续撸业务代码!
所以我们也需要手写一个注解,并且加入到service方法中,告诉代码我是要保证事务的!为了验证我们新建一张日志表作为我们的B表操作,以及我们的实体(ps:这不是真正的日志,而是我们为了演示加进去的对另一张表的操作)
切记:实体也要加入我们的ORM框架的注解,不懂得同学参照上一篇博文
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @ClassName MuyanTransction
* @Description 自定义一个需要事务管理的注解
* @Author dym
* @Date 2019/8/30 10:17
* @Version 1.0
**/
@Target( ElementType.METHOD )
@Retention(RetentionPolicy.RUNTIME)
public @interface MuyanTransction {
}
新建日志实体:
import com.muyan.springboot.myspringtransaction.chg.annotation.MuyanColumn;
import com.muyan.springboot.myspringtransaction.chg.annotation.MuyanId;
import com.muyan.springboot.myspringtransaction.chg.annotation.MuyanTableName;
import lombok.Data;
import lombok.ToString;
import java.util.Date;
/**
* @ClassName OptionLog
* @Description 操作日志表
* @Author dym
* @Date 2019/9/6 10:21
* @Version 1.0
**/
@Data
@ToString
@MuyanTableName(tableName = "option_log")
public class OptionLog {
@MuyanId(idName = "log_id")
private Integer logId;
@MuyanColumn(colomnName = "log_content")
private String logContent;
@MuyanColumn(colomnName = "log_time")
private Date logTime;
}
建表语句
DROP TABLE IF EXISTS `option_log`;
CREATE TABLE `option_log` (
`log_id` int(11) NOT NULL AUTO_INCREMENT,
`log_content` varchar(1000) COLLATE utf8_bin DEFAULT NULL,
`log_time` datetime DEFAULT NULL,
PRIMARY KEY (`log_id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
改造一下我们上篇博文中的service,加入日志操作
import com.muyan.springboot.myspringtransaction.chg.annotation.MuyanTransction;
import com.muyan.springboot.myspringtransaction.chg.domain.OptionLog;
import com.muyan.springboot.myspringtransaction.chg.domain.Student;
import com.muyan.springboot.myspringtransaction.chg.mapper.LogMapper;
import com.muyan.springboot.myspringtransaction.chg.mapper.StudentMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
/**
* @ClassName ChangerUserService
* @Description TODO
* @Author dym
* @Date 2019/8/30 10:16
* @Version 1.0
**/
@Service
public class ChangerStudentService {
@Autowired
private StudentMapper studentMapper;
@Autowired
private LogMapper logMapper;
/**
*@Description 保存实体
*@Author dym
*@Date 2019/9/3 17:53
*@Param [student]
*@return boolean
*/
@MuyanTransction
public boolean saveDataByOrm(Student student){
boolean result = studentMapper.add( student );
OptionLog optionLog = new OptionLog();
optionLog.setLogContent("新增学生数据:"+student.toString());
optionLog.setLogTime( new Date());
logMapper.add( optionLog );
//int i = 1/0; 如果这里打开 则事务全部回滚
return result;
}
/**
*@Description 保存编辑实体
*@Author dym
*@Date 2019/9/3 17:54
*@Param [student]
*@return boolean
*/
@MuyanTransction
public boolean update(Student student){
boolean result = studentMapper.update( student );
OptionLog optionLog = new OptionLog();
optionLog.setLogContent("变更学生数据:"+student.toString());
optionLog.setLogTime( new Date());
logMapper.add( optionLog );
return result;
}
/**
*@Description 根据id删除实体
*@Author dym
*@Date 2019/9/3 17:55
*@Param [id]
*@return boolean
*/
@MuyanTransction
public boolean delete(int id){
boolean result = studentMapper.delete( id );
OptionLog optionLog = new OptionLog();
optionLog.setLogContent("删除ID为:"+id+"的学生数据:");
optionLog.setLogTime( new Date());
logMapper.add( optionLog );
return result;
}
/**
*@Description 根据ID查询student实体
*@Author dym
*@Date 2019/9/3 17:56
*@Param [id]
*@return com.muyan.springboot.myspringtransaction.chg.domain.Student
*/
public Student query(int id){
Student student = (Student) studentMapper.query( id );
return student;
}
/**
*@Description 查询所有的学生列表
*@Author dym
*@Date 2019/9/3 17:58
*@Param []
*@return java.util.List<com.muyan.springboot.myspringtransaction.chg.domain.Student>
*/
public List<Student> queryAll(){
List<Student> list = studentMapper.queryAll();
return list;
}
}
我们日志类的Mapper也要继承我们的通用BaseMapper
import com.muyan.springboot.myspringtransaction.chg.common.BaseMapper;
import com.muyan.springboot.myspringtransaction.chg.domain.OptionLog;
import org.springframework.stereotype.Component;
/**
* @ClassName LogMapper
* @Description 日志操作类
* @Author dym
* @Date 2019/9/6 10:25
* @Version 1.0
**/
@Component
public class LogMapper extends BaseMapper<OptionLog> {
}
至此我们的业务代码已经撸完,下面我们如何使用自定义注解呢!
首先我们自己封装一个DataSource
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;
/**
* @ClassName MuyanDataSource
* @Description 数据源定义
* @Author dym
* @Date 2019/9/4 9:07
* @Version 1.0
**/
@Component
@PropertySource( value = "classpath:/application.properties")
public class MuyanDataSource {
@Value( "${spring.datasource.driver}" )
public String driver ;
@Value( "${spring.datasource.url}" )
public String url ;
@Value( "${spring.datasource.username}" )
public String username;
@Value( "${spring.datasource.password}" )
public String password ;
//加载驱动
@PostConstruct
public void initDriver(){
try {
Driver drivers = (Driver) Class.forName( driver ).newInstance();
// 把这个驱动之家注册到驱动管理器内
DriverManager.registerDriver( drivers );
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e){
e.printStackTrace();
}
}
/**
*@Description 获取连接
*@Author dym
*@Date 2019/9/4 9:37
*@Param []
*@return java.sql.Connection
*/
public Connection getConnection(){
Connection connection = null;
try {
connection = DriverManager.getConnection( url, username, password );
} catch (SQLException e) {
e.printStackTrace();
}
return connection;
}
}
这个就是从数据库连接中获取一个连接。
常识问题: 数据库
DataBase
中获取的Connection
是线程不安全的
在多线程环境下,我们的 事务不能交给一个线程不安全的Connection
来操作,Connection
不是线程安全的,它在多线程环境中使用时,会导致数据操作的错乱,特别是有事务的情况.connection.commit()
方法就是提交事务,你
可以想象,在多线程环境中,线程A开启了事务,然后线程B却意外的commit,这该是个多么纠结的情况.
那我们如何保证我们的connection是线程安全的呢!
要编写一个多线程安全(Thread-safe)的程序是困难的,为了让线程共享资源,必须小心地对共享资源进行同步,同步带来一定的效能延迟,而另一方面,在处理同步的时候,又要注意对象的锁定与释放,避免产生死结,种种因素都使得编写多线程程序变得困难。
尝试从另一个角度来思考多线程共享资源的问题,既然共享资源这么困难,那么就干脆不要共享,何不为每个线程创造一个资源的复本。将每一个线程存取数据的行为加以隔离,实现的方法就是给予每个线程一个特定空间来保管该线程所独享的资源
这里我们采用ThreadLocal
,那什么是ThreadLocal
呢
顾名思义它是local variable(线程局部变量)。它的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量。
那我们将我们获取Connection的方法做进一步改进
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.sql.Connection;
/**
* @ClassName MuYanTransctionHolder
* @Description 自定义一个事务管理的工具类
* @Author dym
* @Date 2019/8/30 10:20
* @Version 1.0
**/
@Component //交给spring去管理
public class MuYanTransctionHolder {
//本来线程不安全的,通过ThreadaLocal这么封装一下,立刻就变成了线程的局部变量,不仅仅安全了,
// 还保证了一个线程下面的操作拿到的Connection是同一个对象
private ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
@Autowired
private MuyanDataSource muyanDataSource;
//获取连接
public Connection getConnection(){
try {
if (threadLocal.get() == null){
Connection connection = muyanDataSource.getConnection();
threadLocal.set( connection );
}
} catch (Exception e) {
e.printStackTrace();
}
return threadLocal.get();
}
}
完了我们将Connection的操作提到一个单独的类中MuYanTransctionManager
中
import org.springframework.stereotype.Component;
import java.sql.Connection;
import java.sql.SQLException;
/**
* @ClassName MuYanTransctionManager
* @Description 管理事务的开发,回滚,关闭
* @Author dym
* @Date 2019/8/30 10:32
* @Version 1.0
**/
@Component
public class MuYanTransctionManager {
//开始事务
public void startTx(Connection connection){
try {
if (connection != null){
connection.setAutoCommit( Boolean.FALSE );
}
} catch (SQLException e) {
e.printStackTrace();
}
}
//提交事务
public void commitTx(Connection connection){
try {
if (connection != null){
connection.commit();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
//事务回滚
public void rollBackTx(Connection connection){
try {
if (connection != null) {
connection.rollback();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
//关闭事务
public void closeTx(Connection connection){
try {
if (connection != null) {
connection.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
最后才是我们的重点,加入AOP,那AOP怎么侵入我们的业务代码呢,我们不是自定义了一个注解吗,找到有这个注解的方法,在方法执行前获取Connection,方法执行完成之后,调用Connection的commit来提交事务,并且关闭connection。这样保证了我们的这个方法里面执行的每个sql都是同一个事务。
import com.muyan.springboot.myspringtransaction.chg.annotation.MuyanTransction;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.sql.Connection;
/**
* @ClassName MuYanAopAspect
* @Description 采用aop来给自定义注解赋予意义
* @Author dym
* @Date 2019/8/30 10:41
* @Version 1.0
**/
@Component
@Aspect
public class MuYanAopAspect {
//注入获取连接
@Autowired
private MuYanTransctionHolder muYanTransctionHolder;
//注入事务管理
@Autowired
private MuYanTransctionManager muYanTransctionManager;
/**
*@Description 说明:
* 1、 使用@Around环绕执行 执行前和执行后
* 2、 采用@annotation 使用了MuyanTransction注解的类执行此方法
* 3、 获取连接,这样保证被MuyanTransction注解的方法采用了同一个连接connection
* 4、 同一个connection共有一个事务,如果失败,则全部回滚
*@Author dym
*@Date 2019/8/30 14:39
*@Param [proceedingJoinPoint]
*@return void
*/
@Around( value = "@annotation(muyanTransction)" )
public Object txAroundOption(ProceedingJoinPoint proceedingJoinPoint, MuyanTransction muyanTransction){
//开始获取连接
Connection connection = muYanTransctionHolder.getConnection();
Object proceed = null;
try{
//开始执行前开启事务管理
muYanTransctionManager.startTx( connection );
//开始执行操作
proceed = proceedingJoinPoint.proceed();
//执行之后提交事务
muYanTransctionManager.commitTx( connection );
}catch (Exception e){
e.printStackTrace();
//回滚事务
muYanTransctionManager.rollBackTx( connection );
}catch (Throwable throwable) {
throwable.printStackTrace();
//回滚事务
muYanTransctionManager.rollBackTx( connection );
}finally {
//关闭事务
muYanTransctionManager.closeTx( connection );
}
return proceed;
}
}
看一下我们的启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication
@EnableTransactionManagement
public class MyspringTransactionApplication {
public static void main(String[] args) {
SpringApplication.run( MyspringTransactionApplication.class, args );
}
}
其他文件和上一篇博客一样,此处不再复制过来,如需要详细查看,则返回上篇即可。
上篇没有粘贴代码,这篇把两个的结合源码全部奉上,手写我们自己的ORM框架和Transaction