Spring-事务管理练习,转账(三种)(maven空间)
1、事务
1.1 、事务是什么
事务是应用程序中一系列严密的操作,所有操作必须成功完成,否则在每个操作中所作的所有更改都会被撤消。也就是事务具有原子性,一个事务中的一系列的操作要么全部成功,要么一个都不做。
1.2、事务的四大特性
事务应该具有4个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为ACID特性。
- 原子性(atomicity)。一个事务是一个不可分割的工作单位,事务中包括的操作要么都做,要么都不做。
- 一致性(consistency)。事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。
- 隔离性(isolation)。一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
- 持久性(durability)。持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
1.3、例子
甲乙两个人转钱,正常情况下:甲输入乙的账号,输入金额,然后甲的账户扣除相应的钱,乙的账户扣除相应的钱;
但是就是有特殊情况,甲在输入账号的时候输错了,就会发生一个情况,甲的钱扣掉了,但是乙没有收到钱,肯定就会出现问题;这就是事务相关的问题:
1.4、解决办法:
在进行转账的时候,我们可以进行事务判断,当其中有一方出现异常时,及时的进行回滚(rollback()),在成功后再进行提交(commit())
练习一:不使用事物进行转账出现的数据不一致现象!
建立数据库表
CREATE TABLE `userinfo` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(255) DEFAULT NULL, `birthday` datetime DEFAULT NULL, `sex` varchar(1) DEFAULT NULL, `address` varchar(255) DEFAULT NULL, `money` int(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of userinfo -- ---------------------------- BEGIN; INSERT INTO `userinfo` VALUES (1, '旺财', '2020-09-29 00:00:00', '女', '五一广场', 2100); INSERT INTO `userinfo` VALUES (2, '来福', '2020-09-16 00:00:00', '男', '五一新干线', 700); COMMIT; SET FOREIGN_KEY_CHECKS = 1;
导入jar包
<dependency> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-core</artifactId> <version>1.4.0</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.6</version> <scope>runtime</scope> </dependency> <dependency> <groupId>net.sf.ehcache</groupId> <artifactId>ehcache-core</artifactId> <version>2.6.11</version> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> </exclusion> </exclusions> </dependency> <!-- https://mvnrepository.com/artifact/org.mybatis.caches/mybatis-ehcache --> <dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-ehcache</artifactId> <version>1.1.0</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.4.5</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.6</version> <scope>runtime</scope> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.12</version> </dependency> <dependency> <groupId>commons-beanutils</groupId> <artifactId>commons-beanutils</artifactId> <version>1.9.3</version> </dependency> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency> <dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>2.6</version> </dependency> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.1.1</version> </dependency> <dependency> <groupId>net.sf.ezmorph</groupId> <artifactId>ezmorph</artifactId> <version>1.0.6</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.41</version> </dependency> <!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload --> <dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.4</version> </dependency> <dependency> <groupId>net.sf.json-lib</groupId> <artifactId>json-lib</artifactId> <version>2.2.3</version> <classifier>jdk15</classifier><!-- 指定jdk版本 --> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> </dependency> <dependency> <groupId>org.apache.taglibs</groupId> <artifactId>taglibs-standard-spec</artifactId> <version>1.2.5</version> </dependency> <dependency> <groupId>org.apache.taglibs</groupId> <artifactId>taglibs-standard-impl</artifactId> <version>1.2.5</version> </dependency> <!-- https://mvnrepository.com/artifact/commons-logging/commons-logging --> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.2</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-aop --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>5.3.2</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-beans --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>5.3.2</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-context --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.2</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-core --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>5.3.2</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-expression --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-expression</artifactId> <version>5.3.2</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-test --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.3.2</version> <scope>test</scope> </dependency> <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.3</version> </dependency>
创建Userinfo实体类---与表字段类型一致
public class Userinfo { private Integer id; private String username; private Date birthday; private String sex; private String address; private Integer money; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username == null ? null : username.trim(); } public Date getBirthday() { return birthday; } public void setBirthday(Date birthday) { this.birthday = birthday; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex == null ? null : sex.trim(); } public String getAddress() { return address; } public void setAddress(String address) { this.address = address == null ? null : address.trim(); } public Integer getMoney() { return money; } public void setMoney(Integer money) { this.money = money; } @Override public String toString() { return "Userinfo{" + "id=" + id + ", username='" + username + '\'' + ", birthday=" + birthday + ", sex='" + sex + '\'' + ", address='" + address + '\'' + ", money=" + money + '}'; } }
创建转账接口
public interface Changmoney { boolean giveMoney(int on,int to,int moeny); }
使用generator.xml创建接口的数据库语句
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"> <generatorConfiguration> <context id="testTables" targetRuntime="MyBatis3"> <commentGenerator> <!-- 是否去除自动生成的注释 true:是 : false:否 --> <property name="suppressAllComments" value="true" /> </commentGenerator> <!--数据库连接的信息:驱动类、连接地址、用户名、密码 --> <jdbcConnection driverClass="com.mysql.jdbc.Driver" connectionURL="jdbc:mysql://localhost:3306/t142" userId="root" password="123456"> </jdbcConnection> <!-- <jdbcConnection driverClass="oracle.jdbc.OracleDriver" connectionURL="jdbc:oracle:thin:@127.0.0.1:1521:yycg" userId="yycg" password="yycg"> </jdbcConnection> --> <!-- 默认false,把JDBC DECIMAL 和 NUMERIC 类型解析为 Integer,为 true时把JDBC DECIMAL 和 NUMERIC 类型解析为java.math.BigDecimal --> <javaTypeResolver> <property name="forceBigDecimals" value="false" /> </javaTypeResolver> <!-- targetProject:生成PO类的位置 --> <javaModelGenerator targetPackage="pojo" targetProject=".\src"> <!-- enableSubPackages:是否让schema作为包的后缀 --> <property name="enableSubPackages" value="false" /> <!-- 从数据库返回的值被清理前后的空格 --> <property name="trimStrings" value="true" /> </javaModelGenerator> <!-- targetProject:mapper映射文件生成的位置 --> <sqlMapGenerator targetPackage="cc.mapper" targetProject=".\src"> <!-- enableSubPackages:是否让schema作为包的后缀 --> <property name="enableSubPackages" value="false" /> </sqlMapGenerator> <!-- targetPackage:mapper接口生成的位置 --> <javaClientGenerator type="XMLMAPPER" targetPackage="cc.mapper" targetProject=".\src"> <!-- enableSubPackages:是否让schema作为包的后缀 --> <property name="enableSubPackages" value="false" /> </javaClientGenerator> <!--指定数据库表--> <table tableName="userinfo" domainObjectName="Userinfo" enableCountByExample="false" enableDeleteByExample="false" enableSelectByExample="false" enableUpdateByExample="false"></table> <!--<table tableName="test" domainObjectName="Test" enableCountByExample="false" enableDeleteByExample="false"--> <!--enableSelectByExample="false" enableUpdateByExample="false"></table>--> </context> </generatorConfiguration>
创建运行generator.xml的Generator
public class Generator {
/**
* @param args
*/
public static void main(String[] args) throws Exception{
List<String> warnings = new ArrayList<String>();
boolean overwrite = true;
File configFile = new File("D:\\IDe\\SpringzhuanzTest\\src\\config\\generator.xml");
ConfigurationParser cp = new ConfigurationParser(warnings);
Configuration config = cp.parseConfiguration(configFile);
DefaultShellCallback callback = new DefaultShellCallback(overwrite);
MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config,
callback, warnings);
myBatisGenerator.generate(null);
}
}
将创建的UserinfoMapper.xml放在config/mapper里,将里面的pojo替换成entity
创建Sqlsession链接数据库的xml文件
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <settings> <!--默认都是false--> <!--<setting name="lazyLoadingEnabled" value="true"/>--> <!--<setting name="aggressiveLazyLoading" value="false"/>--> <!-- 开启二级缓存总开关 --> <setting name="cacheEnabled" value="true"/> </settings> <typeAliases> <package name="pojo"/> </typeAliases> <!-- 配置mybatis的环境信息 --> <environments default="development"> <environment id="development"> <!-- 配置JDBC事务控制,由mybatis进行管理 --> <transactionManager type="JDBC"></transactionManager> <!-- 配置数据源,采用dbcp连接池 --> <dataSource type="POOLED"> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://127.0.0.1:3306/t142?useUnicode=true&characterEncoding=utf8"/> <property name="username" value="root"/> <property name="password" value="123456"/> </dataSource> </environment> </environments> <!-- 加载mapper --> <mappers> <package name="cc.mapper"/> </mappers> </configuration>
创建impl实现接口方法
public class ChangeMoneyImpl implements Changmoney {
public static SqlSession getSqlSession(){
String resource = "SqlMapConfig.xml";
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream(resource);
} catch (IOException e) {
e.printStackTrace();
}
// 2、根据配置文件创建SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
return sqlSessionFactory.openSession(true);
}
@Override
public boolean giveMoney(int on, int to, int moeny) {
SqlSession sqlSession = getSqlSession();
UserinfoMapper mapper = sqlSession.getMapper(UserinfoMapper.class);
//转钱人信息
Userinfo userinfo = mapper.selectByPrimaryKey(on);
int i=0;
if (userinfo!=null){
//设置转钱人的鱼儿
userinfo.setMoney(userinfo.getMoney()-moeny);
i = mapper.updateByPrimaryKey(userinfo);
}
//设置被转钱的信息
Userinfo userinfo1 = mapper.selectByPrimaryKey(to);
int j=0;
if (userinfo1!=null){
//设置被转钱人的鱼儿
userinfo1.setMoney(userinfo1.getMoney()+moeny);
//受影响的行数
j = mapper.updateByPrimaryKey(userinfo1);
}
if (i>0&&j>0){
System.out.println("转钱成功");
return true;
}else {
System.out.println("转钱失败");
return false;
}
}
}
编写测试Sqlsession类
public class TestSqlSession { public static void main(String[] args) { SqlSession sqlSession = ChangeMoneyImpl.getSqlSession(); UserinfoMapper mapper = sqlSession.getMapper(UserinfoMapper.class); Userinfo userinfo = mapper.selectByPrimaryKey(1); System.out.println(userinfo.toString()); } }
OK,数据能显示出来
Userinfo{id=1, username='旺财', birthday=Tue Sep 29 00:00:00 CST 2020, sex='女', address='五一广场', money=200} Process finished with exit code 0
编辑转账测试
public class TestZhuangz { public static void main(String[] args) { Changmoney changmoney = new ChangeMoneyImpl(); boolean b = changmoney.giveMoney(1, 2, 200); System.out.println(b); } }
转账成功
转钱成功 true Process finished with exit code 0
练习二:手工的方式加入mybatis的事物控制,解决数据不一致的问题,但是这种代码入侵性很强,通过AOP思想进行优化
在上面的代码上进行修改
加入输入错误进行回滚
public class ChangeMoneyImpl implements Changmoney { public static SqlSession getSqlSession(){ String resource = "SqlMapConfig.xml"; InputStream inputStream = null; try { inputStream = Resources.getResourceAsStream(resource); } catch (IOException e) { e.printStackTrace(); } // 2、根据配置文件创建SqlSessionFactory SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); return sqlSessionFactory.openSession(true); } @Override public boolean giveMoney(int on, int to, int moeny) { SqlSession sqlSession = getSqlSession(); UserinfoMapper mapper = sqlSession.getMapper(UserinfoMapper.class); //转钱人信息 Userinfo userinfo = mapper.selectByPrimaryKey(on); int i=0; if (userinfo!=null){ //设置转钱人的鱼儿 userinfo.setMoney(userinfo.getMoney()-moeny); i = mapper.updateByPrimaryKey(userinfo); } //设置被转钱的信息 Userinfo userinfo1 = mapper.selectByPrimaryKey(to); int j=0; if (userinfo1!=null){ //设置被转钱人的鱼儿 userinfo1.setMoney(userinfo1.getMoney()+moeny); //受影响的行数 j = mapper.updateByPrimaryKey(userinfo1); } if (i>0&&j>0){ System.out.println("转钱成功"); sqlSession.commit(); return true; }else { System.out.println("转钱失败"); sqlSession.rollback(); return false; } } }
测试
public class TestZhuangz { public static void main(String[] args) { Changmoney changmoney = new ChangeMoneyImpl(); boolean b = changmoney.giveMoney(2, 3, 200); System.out.println(b); } }
结果返回转钱失败,并且数据库没有扣2号账户的钱,这就进行了事物的回滚操作
练习三:自己手动写一个事物黑客(事物管理器),通过springHk的配置来对我们方法进行增强。但还是不够好,入侵目标的所有方法,如果想要只入侵目标对象的部分方法,可以通过方法名来进行判断,这种方式比较粗鲁?
在上面的代码上进行修改
加入util油条,把Sqlsession单独拿出来
public class SqlSessionUtil { private static SqlSession sqlSession= null; static { //1、根据mybaits的配置文件构建SqlSessionFactory //需要改成我们自己的SqlMapConfig.xml的路径 String resource = "SqlMapConfig.xml"; InputStream inputStream = null; try { inputStream = Resources.getResourceAsStream(resource); } catch (IOException e) { e.printStackTrace(); } SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); //2、创建SqlSession sqlSession= sqlSessionFactory.openSession(); } public static SqlSession openSession() { return sqlSession; } public static void main(String[] args) { System.out.println(SqlSessionUtil.openSession()); } }
加入Spring自带的事物管理器
public class TransactionManager implements MethodInterceptor{ @Override public Object invoke(MethodInvocation method) throws Throwable { SqlSession sqlSession = SqlSessionUtil.openSession(); //调用目标方法 boolean result = (boolean) method.proceed(); if( method.getMethod().getName().equals("giveMoney")){ if(result) { sqlSession.commit(); System.out.println("====提交事务==="); }else { sqlSession.rollback(); System.out.println("====回滚事务==="); } } return result; } }
加入beans.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 https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd "> <!-- 1、启动注解扫描--> <!-- <context:annotation-config/> --> <context:component-scan base-package="cc"></context:component-scan> <!-- 1)目标 --> <bean id="target" class="cc.change.ChangeMoneyImpl"></bean> <!-- 2)黑客 --> <bean id="transactionManager" class="cc.proxy.TransactionManager"></bean> <!--3)代理 --> <bean id="ChangeMoneyImpProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="proxyInterfaces" value="cc.change.Changmoney"></property> <!-- 1)注入目标对象 --> <property name="target" ref="target"/> <!-- 2)黑客对象 --> <property name="interceptorNames"> <array> <value>transactionManager</value> </array> </property> </bean> </beans>
修改ChangeMoneyImpl 使用Sql油条
public class ChangeMoneyImpl implements Changmoney { @Override public boolean giveMoney(int on, int to, int moeny) { SqlSession sqlSession = SqlSessionUtil.openSession(); UserinfoMapper mapper = sqlSession.getMapper(UserinfoMapper.class); //转钱人信息 Userinfo userinfo = mapper.selectByPrimaryKey(on); int i=0; if (userinfo!=null){ //设置转钱人的鱼儿 userinfo.setMoney(userinfo.getMoney()-moeny); i = mapper.updateByPrimaryKey(userinfo); } //设置被转钱的信息 Userinfo userinfo1 = mapper.selectByPrimaryKey(to); int j=0; if (userinfo1!=null){ //设置被转钱人的鱼儿 userinfo1.setMoney(userinfo1.getMoney()+moeny); //受影响的行数 j = mapper.updateByPrimaryKey(userinfo1); } if (i>0&&j>0){ System.out.println("转钱成功"); return true; }else { System.out.println("转钱失败"); return false; } } }
添加测试方法
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration({"classpath:beans.xml"}) public class TestHKzhuanz { @Autowired @Qualifier("ChangeMoneyImpProxy") private Changmoney change; @Test public void test1(){ boolean b = change.giveMoney(1, 2, 300); System.out.println(b); } }
测试结果
转钱成功 ====提交事务=== true
package cc.change;
public interface Changmoney {
boolean giveMoney(int on,int to,int moeny);
}