一次寻找IBatisNet事务bug的过程
本文的上下文环境
操作系统:Win7 x64 Professional
开发工具:Visual Studio 2017 语言:C#
数据库ORM:IBatisNet 1.6.2
一、前言
这个项目的前端有Web端,公众号,微信小程序,后端是用WCF写的,部署成windows service。后端使用了IBatisNet这样的轻量级ORM框架,sql是写在xml里面的,每个模块都有一个xml文件。sql的返回值对应的实体类配置在同一个xml文件里,像这样。

二、问题出现
这个项目一直运行的很正常,直到有一天发布了某一个功能之后,问题就出现了,异常日志经常看到这样的文字“SqlMap could not invoke BeginTransaction(). A Transaction is already started. Call CommitTransaction() or RollbackTransaction first”,一看代码,这个异常是从SqlMapper.BeginTransaction方法里面抛出的,字面意思就是启动事务,不能正常启动,需要首先调用提交或回滚事务。按着这个逻辑来说,应该是某一次事务忘记提交或回滚了。但是这个错误在本地开发环境复现不了,线上环境时不时的出现。如果本地能够reproduce,应该会比较好解决一些。
三、查看bug起因
我们项目中事务用了PostSharp里面的AOP编程,只要在需要事务的方法上面加个attribute就能实现事务支持。代码如下:
[Serializable]
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class AopTransactionMethodAttribute : MethodInterceptionAspect
{
//调用本函数时截取异常
public override void OnInvoke(MethodInterceptionArgs args)
{
Mapper.Instance().BeginTransaction();
try
{
//执行方法
base.OnInvoke(args);
}
catch
{
Mapper.Instance().RollBackTransaction(); throw;
} TData result = args.ReturnValue as TData; if (result == null || result.Tag <= 0)
{
//回滚事务
Mapper.Instance().RollBackTransaction();
}
else
{
//提交事务
Mapper.Instance().CommitTransaction();
}
}
}
AopTransactionMethodAttribute
这样就是很方便写代码,不用在每个操作数据库多表的地方都显式地调用事务。
一开始怀疑这个地方可能有问题,在try catch的基础上又加了一个try catch finally 用来彻底提交或回归事务。发布到线上之后,错误仍然时不时的出现。
由于只能在线上环境可以重现,考虑只能使用日志来记录执行过程了,我把事务的BeginTransaction,CommitTransaction,RollbackTransaction的执行全部记上日志,考虑业务会并发执行,日志肯定要加锁,并且把每次事务的CallStack和ThreadId给输出来,代码如下:
public static readonly object LockTransaction = new object();
public ISqlMapSession BeginTransaction()
{
lock (LockTransaction)
{
string file = string.Format(AppDomain.CurrentDomain.BaseDirectory + "Log\\{0}\\{1}.log", DateTime.Now.ToString("yyyy-MM"), DateTime.Now.ToString("yyyy-MM-dd"));
System.IO.StreamWriter sw = new System.IO.StreamWriter(file, true);
sw.Write(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
sw.Write(" Begin Transaction Thread Id:" + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString().PadLeft(, ''));
if (_sessionStore.LocalSession != null)
{
sw.WriteLine();
StackTrace trace = new StackTrace(true);
for (int i = ; i < trace.FrameCount; i++)
{
StackFrame frame = trace.GetFrame(i);
MethodBase method = frame.GetMethod();
if (method.DeclaringType != null)
{
sw.WriteLine(method.DeclaringType.FullName + " " + method.Name);
}
else
{
sw.WriteLine(method.Name);
}
}
sw.WriteLine(" SqlMap could not invoke BeginTransaction(). A Transaction is already started. Call CommitTransaction() or RollbackTransaction first");
//throw new DataMapperException("SqlMap could not invoke BeginTransaction(). A Transaction is already started. Call CommitTransaction() or RollbackTransaction first.");
}
ISqlMapSession session = CreateSqlMapSession();
_sessionStore.Store(session);
session.BeginTransaction();
sw.WriteLine();
sw.Close();
return session;
}
}
BeginTransaction
public void CommitTransaction()
{
lock (LockTransaction)
{
string file = string.Format(AppDomain.CurrentDomain.BaseDirectory + "Log\\{0}\\{1}.log", DateTime.Now.ToString("yyyy-MM"), DateTime.Now.ToString("yyyy-MM-dd"));
System.IO.StreamWriter sw = new System.IO.StreamWriter(file, true);
sw.Write(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
sw.Write(" Commit Transaction Thread Id:" + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString().PadLeft(, ''));
if (_sessionStore.LocalSession == null)
{
throw new DataMapperException("SqlMap could not invoke CommitTransaction(). No Transaction was started. Call BeginTransaction() first.");
}
try
{
ISqlMapSession session = _sessionStore.LocalSession;
session.CommitTransaction();
}
finally
{
_sessionStore.Dispose();
}
if (_sessionStore.LocalSession != null)
{
sw.Write(" Commit Transaction Not Successfully " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString().PadLeft(, ''));
}
else
{
sw.Write(" Commit Transaction " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString().PadLeft(, ''));
}
sw.WriteLine();
sw.Close();
}
}
CommitTransaction
public void RollBackTransaction()
{
lock (LockTransaction)
{
string file = string.Format(AppDomain.CurrentDomain.BaseDirectory + "Log\\{0}\\{1}.log", DateTime.Now.ToString("yyyy-MM"), DateTime.Now.ToString("yyyy-MM-dd"));
System.IO.StreamWriter sw = new System.IO.StreamWriter(file, true);
sw.Write(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
sw.Write(" Roll Transaction Thread Id:" + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString().PadLeft(, ''));
if (_sessionStore.LocalSession == null)
{
throw new DataMapperException("SqlMap could not invoke RollBackTransaction(). No Transaction was started. Call BeginTransaction() first.");
}
try
{
ISqlMapSession session = _sessionStore.LocalSession;
session.RollBackTransaction();
}
finally
{
_sessionStore.Dispose();
}
if (_sessionStore.LocalSession != null)
{
sw.Write(" Roll Transaction Not Successfully " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString().PadLeft(, ''));
}
else
{
sw.Write(" Roll Transaction " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString().PadLeft(, ''));
}
sw.WriteLine();
sw.Close();
}
}
RollbackTransaction

结果出乎意料的是,事务一个BeginTransaction后面要么跟一个Rollback Transaction,要么跟一个Commit Transaction,这完全是正确的啊,心里顿时真不是滋味,什么破代码竟然没问题。
不过,问题总归是需要解决的,不然永远都是止步不前。
回到最开始错误的地方,因为在BeginTransaction的时候,_sessionStore.LocalSession != null,抛出了那个错误,我就需要调查这个LocalSession。
因为LocalSession对应的对象SqlMapSession里面没有标识对象唯一性的建,我就在SqlMapSession里面加了一个Id标识,用来确定这个LocalSession到底有没有被销毁。
public SqlMapSession(ISqlMapper sqlMapper)
{
_dataSource = sqlMapper.DataSource;
_sqlMapper = sqlMapper;
lock (IBatisNet.DataMapper.SqlMapper.LockTransaction)
{
id = long.Parse(DateTime.Now.ToString("yyyyMMddHHmmssfff"));
}
}
IBatisNet在保存session的时候,在Winform程序里面使用CallContextSessionStore这个类来获取、保存、销毁session。CallContextSessionStore类里面用CallContext.GetData(name)获取,CallContext.SetData(name, session)保存,CallContext.SetData(name, null)销毁。CallContext这个类支持在同一个线程内,访问的是同一个对象,也就是说,当前线程对对象进行储存到线程本地储存区,对象随着线程的销毁而销毁。
同样地,我把每个方法的调用输出在另外一个日志文件里面,输出在同一个文件会报文件拒绝访问,可能是由于在Dispose的时候,我去调用GetSession所致。代码如下:
public override ISqlMapSession LocalSession
{
get
{
lock (IBatisNet.DataMapper.SqlMapper.LockTransaction)
{
var session = CallContext.GetData(sessionName) as SqlMapSession;
if (session != null)
{
string file = string.Format(AppDomain.CurrentDomain.BaseDirectory + "Log\\{0}\\{1}_1.log", DateTime.Now.ToString("yyyy-MM"), DateTime.Now.ToString("yyyy-MM-dd"));
System.IO.StreamWriter sw = new System.IO.StreamWriter(file, true);
sw.Write(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
sw.Write(" LocalSession Thread Id:" + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString().PadLeft(, ''));
sw.Write(" session Id:" + session.Id);
sw.WriteLine();
sw.Close();
}
return session;
}
}
}
Get Session
public override void Store(ISqlMapSession session)
{
lock (IBatisNet.DataMapper.SqlMapper.LockTransaction)
{
string file = string.Format(AppDomain.CurrentDomain.BaseDirectory + "Log\\{0}\\{1}_1.log", DateTime.Now.ToString("yyyy-MM"), DateTime.Now.ToString("yyyy-MM-dd"));
System.IO.StreamWriter sw = new System.IO.StreamWriter(file, true);
sw.Write(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
sw.Write(" Store Thread Id:" + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString().PadLeft(, ''));
sw.Write(" session Id:" + session.Id);
sw.WriteLine(); StackTrace trace = new StackTrace(true);
for (int i = ; i < trace.FrameCount; i++)
{
StackFrame frame = trace.GetFrame(i);
MethodBase method = frame.GetMethod();
if (method.DeclaringType != null)
{
sw.WriteLine(method.DeclaringType.FullName + " " + method.Name);
}
else
{
sw.WriteLine(method.Name);
}
} sw.Close();
CallContext.SetData(sessionName, session);
}
}
Store Session
public override void Dispose()
{
lock (IBatisNet.DataMapper.SqlMapper.LockTransaction)
{
var session = CallContext.GetData(sessionName) as SqlMapSession;
string file = string.Format(AppDomain.CurrentDomain.BaseDirectory + "Log\\{0}\\{1}_1.log", DateTime.Now.ToString("yyyy-MM"), DateTime.Now.ToString("yyyy-MM-dd"));
System.IO.StreamWriter sw = new System.IO.StreamWriter(file, true);
sw.Write(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
sw.Write(" Dispose Thread Id:" + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString().PadLeft(, ''));
sw.Write(" session Id:" + session.Id);
sw.WriteLine();
sw.Close(); CallContext.SetData(sessionName, null);
}
}
Remove Session
找到上一个日志文件出错的时间,再和这个日志文件对照,顿时发现有一个session id只有Store,没有Dispose。并且通过CallStack知道是哪个方法在调用和使用这个session,简直欣喜若狂啊:)
Finally,问题找到了,是一个业务方法不经意地调用了一个获取Sql的方法,来获取当前执行的sql,但是这个方法的写法有问题,session只有open,没有close。
四、总结
一沙一世界,一叶一菩提
用心去看,去体悟,哪怕再小的事物都可以成为一个独立的空间,哪怕是一粒沙子都可以是一个世界,容纳万物。问题在那里啊?就在那一叶上。
一次寻找IBatisNet事务bug的过程的更多相关文章
- spring internalTransactionAdvisor 事务 advisor 初始化过程
spring internalTransactionAdvisor 事务 advisor 初始化过程:
- 找bug的过程
关于昨天程序出差我找bug的过程记录 昨天才程序 https://www.cnblogs.com/pythonywy/p/11006273.html ├── xxxx │ ├── src.py │ └ ...
- 记录一次bug解决过程:resultType和手动开启事务
一.总结 二.BUG描述:MyBatis中resultType使用 MyBatis中的resultType类似于入参:parameterType.先看IDCM项目中的实际使用案例代码,如下: // L ...
- 年年出妖事,一例由JSON解析导致的"薛定谔BUG"排查过程记录
前言 做开发这么多年,也碰到无数的bug了.不过再复杂的bug,只要仔细去研读代码,加上debug,总能找到原因. 但是最近公司内碰到的这一个bug,这个bug初看很简单,但是非常妖孽,在一段时间内我 ...
- 记录一次bug解决过程:else未补全导致数据泄露和代码优化
一.总结 快捷键ctrl + alt + 四个方向键 --> 倒置屏幕 未补全else逻辑,倒置查询数据泄露 空指针是最容易犯的错误,数据的空指针,可以普遍采用三目运算符来解决 SVN冲突解决关 ...
- 记录一次bug解决过程:velocity中获取url中的参数
一.总结 在Webx的Velocity中获取url中参数:$rundata.getRequest().getParameter('userId') 在Webx项目中,防止CSRF攻击(Cross-si ...
- 【Unity游戏开发】记一次解决 LuaFunction has been disposed 的bug的过程
一.引子 RT,本篇博客记录的是马三的一次解决 LuaFunction has been disposed 的bug的全过程,事情还要从马三的自研框架 ColaFrameWork 说起.最近,马三在业 ...
- CentOS 7.1系统自动重启的Bug定位过程
[问题] 有同事反应最近有多台MongoDB的服务器CentOS 7.1系统会自动重启,分析了下问题原因. [排查过程] 1. 检查系统日志/var/log/message,并没有记录异常信息,jou ...
- Mysql视图、触发器、事务、储存过程、函数
一.视图 什么是视图 视图是有一张表或多张表的查询结果构成的一张虚拟表 为什么使用视图 当我们在使用多表查询时 我们的sql语句可能会非常的复杂,如果每次都编写一遍sql'的话无疑是一件麻烦的事情,这 ...
随机推荐
- CF400C/[思维题]
题目链接http://codeforces.com/problemset/problem/400/C 题意:给出一个(N,M)矩形和矩形里的p(p<=1e5)个点坐标,然后顺时针旋转x,镜面对称 ...
- 【BZOJ 3235】 3235: [Ahoi2013]好方的蛇 (单调栈+容斥原理)
3235: [Ahoi2013]好方的蛇 Time Limit: 10 Sec Memory Limit: 64 MBSubmit: 187 Solved: 95 Description 有一天, ...
- 【Splay 总结】
很多年前学的splay已经忘了? BZOJ 3729 要用Splay啊哭..强制在线.. 二叉查找树 二叉排序树(Binary Sort Tree)又称二叉查找树(Binary Search Tree ...
- luogu P1965 转圈游戏
题目描述 n 个小伙伴(编号从 0 到 n-1)围坐一圈玩游戏.按照顺时针方向给 n 个位置编号,从0 到 n-1.最初,第 0 号小伙伴在第 0 号位置,第 1 号小伙伴在第 1 号位置,……,依此 ...
- BZOJ4029 HEOI2015定价
贪心. 每次将最后一个非零位加一判断即可. 一开始想少了,只关心把最后一位变成5了,其实可以都变的. #include<bits/stdc++.h> using namespace std ...
- 「BZOJ 2534」 L - gap字符串
「BZOJ 2534」 L - gap字符串 题目描述 有一种形如 \(uv u\) 形式的字符串,其中 \(u\) 是非空字符串,且 \(v\) 的长度正好为 \(L\), 那么称这个字符串为 \( ...
- HDU1251 统计难题 trie树 简单
http://acm.hdu.edu.cn/showproblem.php?pid=1251 题意: 找前缀数量 裸模板 #include<cstdio> #include<cstr ...
- ZOJ 3707 Calculate Prime S 数论
思路:容易得到s[n]=s[n-1]+s[n-2],也就是fib数. 求第k小的fib质数的也就是第k个质数数-2,当k>2时. 在就是s[n]/x%m=s[n]%(x*m)/x. 代码如下: ...
- jQuery对象和Javascript对象
jQuery 对象是通过 jQuery 包装DOM 对象后产生的对象.jQuery 对象是 jQuery 独有的,其可以使用 jQuery 里的方法,但是不能使用 DOM 的方法:例如: $(&quo ...
- localhost与127.0.0.1的区别 2
localhost与127.0.0.1的区别localhost与127.0.0.1的区别是什么?相信有人会说是本地ip,曾有人说,用127.0.0.1比localhost好,可以减少一次解析.看来这个 ...