数据库学的不好,之前写项目,习惯性的将所有操作放在controller中,运行没问题,就在前15分钟看MySql事务的时候,为了测试下事务的原理机制,在3个添加操作中加了一个异常 int a = 10/0; 他不说说事务可以要么全部成功要么全部失败嘛,那就试试,试试就试试。
结果显而一见,查询数据库的时候发现一条数据添加进去了,完全违背了我之前学的事务的机制啊,检查代码,发现我也采用aop机制配置了事务的啊
<!-- 启用事务处理配置 采取aop切面编程处理 -->
<!-- 开启AOP注解扫描 -->
<aop:aspectj-autoproxy proxy-target-class="true" />
<!-- 事务管理器,依赖于数据源 -->
<bean id="txManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 进入到了事务的配置声明 -->
<tx:annotation-driven transaction-manager="txManager" />
<!-- 编写通知:对事务进行增强(通知),需要编写对切入点和具体执行事务细节 -->
<tx:advice id="txAdvice" transaction-manager="txManager">
<!--
为切入点方法添加事务详情
name:方法名,*表示任意方法名称
propagation:设置传播行为
isolation:设置隔离级别
read-only:是否只读
-->
<tx:attributes>
<tx:method name="insert*" propagation="REQUIRED" />
<tx:method name="update*" propagation="REQUIRED" />
<tx:method name="delete*" propagation="REQUIRED" />
<tx:method name="add*" propagation="REQUIRED" />
<tx:method name="edit*" propagation="REQUIRED" />
<tx:method name="change*" propagation="REQUIRED" />
<tx:method name="remove*" propagation="REQUIRED" />
<tx:method name="login*" propagation="REQUIRED" />
<tx:method name="rm*" propagation="REQUIRED" />
<tx:method name="get*" propagation="REQUIRED" read-only="true" />
<tx:method name="check*" propagation="REQUIRED" read-only="true" />
<tx:method name="load*" propagation="REQUIRED" read-only="true" />
<tx:method name="list*" propagation="REQUIRED" read-only="true" />
<tx:method name="*" propagation="REQUIRED" read-only="true" />
</tx:attributes>
</tx:advice>
<!-- 设置AOP,让Spring自动对目标生成代理,需要使用AspectJ表达式 -->
<!-- 定义事务的处理切入点 -->
<aop:config expose-proxy="true">
<aop:pointcut expression="execution(* cn.linkpower.service..*.*(..))" id="pointcut"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="pointcut"/>
</aop:config>
于是我逐个分析流程,才知道之前学的都大错特错了,引以为戒,并向大家以代码的风格大致说说何为事务?
百度上关于事务的讲解:
MySQL 事务主要用于处理操作量大,复杂度高的数据。比如说,在人员管理系统中,你删除一个人员,你即需要删除人员的基本资料,也要删除和该人员相关的信息,如信箱,文章等等,这样,这些数据库操作语句就构成一个事务!
在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务。
事务处理可以用来维护数据库的完整性,保证成批的 SQL 语句要么全部执行,要么全部不执行。
事务用来管理 insert,update,delete 语句
一般来说,事务是必须满足4个条件(ACID)::原子性(Atomicity,或称不可分割性)、一致性(Consistency)、隔离性(Isolation,又称独立性)、持久性(Durability)。
原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失
说了那么多,远远没有实际代码来的实在,上代码(这里的代码是我修改之后的代码,因为aop配置切点为所有的serviceImpl层)
@Override
public boolean insertProductListAndModes(Map<String, Object> parameterMaps) {
String productTypeVal = (String) parameterMaps.get("productTypeVal");
String productModelVal = (String) parameterMaps.get("productModelVal");
String productKey = (String) parameterMaps.get("productKey");
String productSN = (String) parameterMaps.get("productSN");
String hexBatch = (String) parameterMaps.get("hexBatch");
String productQrCode = (String) parameterMaps.get("productQrCode");
String mac = (String) parameterMaps.get("mac");
String imei = (String) parameterMaps.get("imei");
String imsi = (String) parameterMaps.get("imsi");
ProductList productList = new ProductList();
productList.setProductType(productTypeVal);
productList.setProductCode(productModelVal);
productList.setProductKey(productKey);
productList.setProductSn(productSN);
productList.setBatch(hexBatch);
productList.setCreateDate(StringUtil.getCreateTimes());
productList.setProductQrcode(productQrCode);
int productId = productListDao.insertAndGetId(productList);
log.info("创建设备 新生成id--->" + productId);
log.info("创建设备 新生成id--->" + productList.getId());
// 2、将获取到的对应id信息和模块信息填入product_modes表中
if (productId <= 0) {
return false;
}
Integer insertProductId = productList.getId();
// 将mac imei等信息保存modes表中
ProductMods productMods = null;
if (!StringUtil.isEmpty(mac)) {
productMods = new ProductMods();
productMods.setProductId(insertProductId);
productMods.setModuleType("02");
productMods.setMac(mac.toUpperCase());
int insertModsVal = productModesDao.insertMods(productMods);
if(insertModsVal <= 0){
return false;
}
}
//测试事务
int a = 10/0;
// imei存在
if (!StringUtil.isEmpty(imei)) {
productMods = new ProductMods();
productMods.setProductId(insertProductId);
productMods.setModuleType("01");
productMods.setImei(Long.parseLong(imei));
// 判断imsi是否存在
if (!StringUtil.isEmpty(imsi)) {
productMods.setImsi(Long.parseLong(imsi));
}
int insertModsVal = productModesDao.insertMods(productMods);
if(insertModsVal <= 0){
return false;
}
}
return true;
}
这里面看我的注释,我加入了一个 事务测试
//测试事务
int a = 10/0;
正常来说上面的增加操作有mac时,两个添加是会成功的,按照我以前学废的思维,数据库是会有2条新纪录的,但是真的是这样吗?
程序运行测试
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@34a15177] was not registered for synchronization because synchronization is not active
JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@2ed532c1] will not be managed by Spring
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@34a15177]
2019-03-18 22:49:19 -41573 [http-nio-80-exec-28] INFO - 创建设备 新生成id--->1
2019-03-18 22:49:19 -41574 [http-nio-80-exec-28] INFO - 创建设备 新生成id--->1000000015
2019-03-18 22:49:19 -41584 [http-nio-80-exec-28] INFO - 异常信息----------->java.lang.ArithmeticException: / by zero
2019-03-18 22:49:19 -41585 [http-nio-80-exec-28] ERROR - 异常信息----------->java.lang.ArithmeticException: / by zero
2019-03-18 22:49:19 -41594 [http-nio-80-exec-28] INFO - -------------->afterCompletion
中间sql比较关键哈,公司源码,不能外泄,但不影响分析!
从日志信息可以看出来,添加第一条数据成功了的,得到了一个添加后的哪个id值;
第二条其实也是执行成功了的,我的异常加载最后个增加操作前的。
但是回去数据库,刷新再刷新,,,,继续刷新,空有id,但第一条添加操作无数据!!!再看另外一张表,完全没有新数据
想必大家也看明白了吧,同一个业务对数据库的增删改操作,如果处理操作过多,有一处失败,其他所有操作均失败!
自我感觉:
aop配置了切面切点,在切点处配置了execution表达式,execution字面意思就是执行,也就是说在insert*操作执行时,他回去监听,如果全部成功就会执行保存操作,一个失败,他就回滚!就像你写文件,你空有写进去信息,但你关闭文件时选择了取消保存,文件中有数据吗?
问公司一位运维大佬,运维大佬大致给我讲了下linux文件修改机制,当你编辑文件的时候,linux系统自动给你生成了一个 .back 格式的文件,当你执行了修改 且 :wq! 后,他会将之前创建的 .back 文件删除,运用新文件;如果你没保存 :q! ,他就会使用 .back 文件覆盖你修改的文件(相当于数据库的数据回滚操作)
2019.03.19 23:35增加新笔记
上面的截图,事务中添加数据库有个图是有问题的,今天反复琢磨了下,要么全成功,要么全失败,一个失败了,数据库之前添加的数据应该是全部回滚,而不是有数据产生。
具体配置文件(反复测试实验过,真实度大家放心)
web.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
id="WebApp_ID" version="3.0">
<display-name>LinkpowerLockCenter</display-name>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- 静态文件不拦截 -->
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.js</url-pattern>
<url-pattern>*.css</url-pattern>
<url-pattern>/assets/*"</url-pattern>
<url-pattern>/static/*</url-pattern>
</servlet-mapping>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:application-context.xml,
</param-value>
</context-param>
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:application-mvc.xml</param-value>
</init-param>
<!-- 设置加载级别 -->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<filter>
<filter-name>encoding</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encoding</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
<filter-name>DruidWebStatFilter</filter-name>
<filter-class>com.alibaba.druid.support.http.WebStatFilter</filter-class>
<init-param>
<param-name>exclusions</param-name>
<param-value>*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>DruidWebStatFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<servlet>
<servlet-name>DruidStatView</servlet-name>
<servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class>
<init-param>
<param-name>resetEnable</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>loginUsername</param-name>
<param-value>admin</param-value>
</init-param>
<init-param>
<param-name>loginPassword</param-name>
<param-value>admin</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>DruidStatView</servlet-name>
<url-pattern>/druid/*</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
</web-app>
application-context.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:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:task="http://www.springframework.org/schema/task"
xsi:schemaLocation="
http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task-4.1.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.1.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.1.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.1.xsd">
<!-- 引入数据库链接配置文件 -->
<context:property-placeholder location="classpath:database.properties" />
<!-- 配置数据源 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${db.driver}" /> <!-- 驱动程序 -->
<property name="url" value="${db.url}" /><!-- 连接地址 -->
<property name="username" value="${db.user}" /> <!-- 用户名 -->
<property name="password" value="${db.password}" /> <!-- 密码 -->
<!-- 最大连接数 -->
<property name="maxActive" value="${db.maxActive}" />
<!-- 初始化连接数量 -->
<property name="initialSize" value="${db.initialSize}" />
<!-- 配置获取连接等待超时的时间 -->
<property name="maxWait" value="${db.maxWait}" />
<!-- 最小连接数 -->
<property name="minIdle" value="${db.minIdle}" />
<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
<!-- 有两个含义: 1)Destroy线程会检测连接的间隔时间2)testWhileIdle的判断依据,详细看testWhileIdle属性的说明 -->
<property name="timeBetweenEvictionRunsMillis" value="${db.timeBetweenEvictionRunsMillis}" />
<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="${db.minEvictableIdleTimeMillis}" />
<!-- 用来检测连接是否有效的sql,要求是一个查询语句。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会其作用。 -->
<property name="validationQuery" value="${db.validationQuery}" />
<property name="testWhileIdle" value="${db.testWhileIdle}" />
<!-- 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。默认true -->
<property name="testOnBorrow" value="${db.testOnBorrow}" />
<!-- 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能 默认false -->
<property name="testOnReturn" value="${db.testOnReturn}" />
<!-- 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100 -->
<property name="maxOpenPreparedStatements" value="${db.maxOpenPreparedStatements}" />
<!-- 打开removeAbandoned功能 -->
<property name="removeAbandoned" value="${db.removeAbandoned}" />
<!-- 1800秒,也就是30分钟 -->
<property name="removeAbandonedTimeout" value="${db.removeAbandonedTimeout}" />
<!-- 关闭abanded连接时输出错误日志 -->
<property name="logAbandoned" value="${db.logAbandoned}" />
<!-- 属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有:
监控统计用的filter:stat日志用的filter:log4j防御sql注入的filter:wall -->
<property name="filters" value="${db.filters}" />
</bean>
<!-- 注册SqlSessionFactoryBean -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<!-- 自动扫描mappers.xml文件 -->
<property name="mapperLocations" value="classpath:cn/linkpower/dao/*.xml" />
<property name="configLocation" value="classpath:mybatis-config.xml"></property>
</bean>
<!-- DAO接口所在包名,Spring会自动查找其下的类 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!-- 扫描dao包 -->
<property name="basePackage" value="cn.linkpower.dao" />
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
</bean>
<!-- context.xml配置扫描所有包 -->
<context:component-scan base-package="cn.linkpower" />
<!-- 启用事务处理配置 采取aop切面编程处理 -->
<!-- 开启AOP注解扫描 -->
<!-- <aop:aspectj-autoproxy proxy-target-class="true" /> -->
<!-- 设置AOP,让Spring自动对目标生成代理,需要使用AspectJ表达式 expose-proxy="true" -->
<!-- 定义事务的处理切入点 -->
<aop:config >
<aop:pointcut expression="execution(* cn.linkpower.service.impl.*.*(..))" id="pointcut"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="pointcut"/>
</aop:config>
<!-- 进入到了事务的配置声明 -->
<tx:annotation-driven transaction-manager="transactionManager" />
<!-- 编写通知:对事务进行增强(通知),需要编写对切入点和具体执行事务细节 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- <tx:method name="addProductListAndModes" propagation="REQUIRED" read-only="false" /> -->
<tx:method name="add*" propagation="REQUIRED" read-only="false" />
<tx:method name="insert*" propagation="REQUIRED" read-only="false" />
<tx:method name="update*" propagation="REQUIRED" read-only="false" />
<tx:method name="delete*" propagation="REQUIRED" read-only="false" />
<tx:method name="edit*" propagation="REQUIRED" read-only="false" />
<tx:method name="change*" propagation="REQUIRED" read-only="false" />
<tx:method name="remove*" propagation="REQUIRED" read-only="false" />
<tx:method name="login*" propagation="REQUIRED" read-only="false" />
<tx:method name="rm*" propagation="REQUIRED" read-only="false" />
<tx:method name="get*" propagation="REQUIRED" read-only="true" />
<tx:method name="check*" propagation="REQUIRED" read-only="true" />
<tx:method name="load*" propagation="REQUIRED" read-only="true" />
<tx:method name="list*" propagation="REQUIRED" read-only="true" />
<tx:method name="*" propagation="REQUIRED" read-only="true" />
</tx:attributes>
</tx:advice>
<!-- 事务管理器,依赖于数据源 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
application-mvc.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:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:task="http://www.springframework.org/schema/task"
xsi:schemaLocation="
http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task-4.1.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.1.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.1.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.1.xsd">
<!-- 启动自动扫描 -->
<context:annotation-config/>
<!-- 注册MVC注解驱动 -->
<mvc:annotation-driven />
<!-- 静态资源可访问的设置方式 -->
<mvc:default-servlet-handler />
<!-- 扫描包 springmvc只扫描到controller -->
<context:component-scan base-package="cn.linkpower.controller" />
<mvc:interceptors> <!-- 定义拦截器栈,可以定义有多个拦截器 -->
<mvc:interceptor> <!-- 定义某一个具体的拦截器 -->
<mvc:mapping path="/dtu/**/*"/> <!-- 该拦截器针对于所有路径下的action -->
<!-- 定义该拦截器使用的拦截器处理程序类,必须是HandlerInterceptor子类 -->
<bean class="cn.linkpower.handler.ValidationInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
<!-- 配置全局异常处理类 -->
<bean class="cn.linkpower.exception.GloablException"></bean>
<!-- 读取自定义配置文件信息 id="messageSource" 与写的全局 cn.linkpower.controller.abs.AbsController 中的相对应 -->
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<array>
<value>Messages</value>
</array>
</property>
</bean>
<!-- 配置视图解析器,可以显式设置,也可以不设置,不设置会依据SpringMVC的默认设置 -->
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
</bean>
</beans>
细心的人应该可以发现我的配置文件,context和mvc的配置扫描包时,有两个差不多的,但却有区别:
context中的扫描包配置,采取扫描cn.linkpower父包路径下的所有像controller、vo、dao、service等包
<!-- context.xml配置扫描所有包 -->
<context:component-scan base-package="cn.linkpower" />
但在mvc配置文件中的扫描为,只扫描service(具体的业务实现类)
<!-- 扫描包 springmvc只扫描到controller -->
<context:component-scan base-package="cn.linkpower.controller" />
为什么会有这种说法,参考博客某位大佬的博客经验,他说了一句话
spring-MVC.xml中要扫具体包只扫到Controller, 因为spring-MVC.xml是输入dispatcherServlet的,
而applicationContext.xml,applicationContext-tran.xml是输入contextLoaderListener的,
因此spring-MVC.xml的加载要晚于它俩,会造成本已增加过的方法被覆盖
仔细想了下,是这个意思,一般mvc的配置,在web.xml配置中指明是org.springframework.web.servlet.DispatcherServlet的
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:application-mvc.xml</param-value>
</init-param>
<!-- 设置加载级别 -->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
context配置数据库连接池、事务等信息,他是基于contextLoaderListener 的,加载顺序上的确是先进行了contextLoaderListener 的加载优先,所以context(数据库连接池、事务等)的配置先就加载好了,可以使用扫描整包的形式;但 mvc的配置文件中一般配置视图解析器,DispatcherServlet 后加载实现,如果也配置扫描整包,将会导致之前context加载的数据文件在spring容器中造成覆盖!!