Asp.net并发请求导致的数据重复插入问题
前段时间工作中,有客户反应了系统中某类待办重复出现两次的情况。我核实了数据之后,分析认为是并发请求下导致的数据不一致性问题,并做了重现。其实这并不是一个需要频繁调用的功能,但是客户连续点击了两次,导致出现了并发问题。除了前端优化,这里重点探讨后台方面代码层面的处理,最终解决问题。
一、情景分析
Asp.net程序部署Web服务,是多主线程并发执行的,当多个用户请求进入同一个后台函数时,后进入的请求有可能会获取到非最新状态的数据。
结合我遇到的实际情况举个例子,假设后台函数Func1,先读取表TableA,TableB的数据,进行处理后,存入TableB中,而数据库事务执行会在函数结束前才提交。请求Req1执行Func1提交事务之前,Req2又进入Func1并读取了TableA,TableB的数据,这时Req1执行完成,这就相当于Req2拿到的已经是旧的数据,在旧的数据的基础上再做数据处理操作,结果自然不正确了。
说到这里,你可能还不能想象具体会出现什么问题,而确实这种并发情况在非幂等功能下才会导致数据错误,下面就举实例说明。
二、实例重现
现在有数据表Info,Info2,Info2的数据就是基于Info表数据产生的,两个表都有字段-证件号码IdentNo。
函数SyncWork()的功能为:
1,读取Info表和Info2表中共同的IdentNo行数据,将Info表中的其它字段同步到Info2表;
2,读取Info表中有,而Info2表中没有的IdentNo行数据,将这些数据插入Info2表。
表实体代码实现如下:
/// <summary>
/// 信息表
/// </summary>
public class Info
{
public int Id { get; set; }
/// <summary>
/// 证件号码
/// </summary>
public string IdentNo { get; set; }
/// <summary>
/// 姓名
/// </summary>
public string Name { get; set; }
/// <summary>
/// 爱好
/// </summary>
public string Hobby { get; set; }
/// <summary>
/// 备注信息
/// </summary>
public string Bz { get; set; }
} /// <summary>
/// 信息表2
/// </summary>
public class Info2
{
public int Id { get; set; }
/// <summary>
/// 证件号码
/// </summary>
public string IdentNo { get; set; }
/// <summary>
/// 姓名
/// </summary>
public string Name { get; set; }
/// <summary>
/// 爱好
/// </summary>
public string Hobby { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreateTime { get; set; }
/// <summary>
/// 最后修改时间
/// </summary>
public DateTime? UpdateTime { get; set; }
/// <summary>
/// 评分
/// </summary>
public int? Score { get; set; }
}
SyncWork()代码实现如下,代码中加入了辅助的输出信息:
public static string SyncWork()
{
StringBuilder sb = new StringBuilder();
//
int threadId = Thread.CurrentThread.ManagedThreadId;
sb.AppendLine($"{DateTime.Now.ToString("HH:mm:ss:fff")}.线程Id:{threadId}");
//
MyDbContext db = new MyDbContext();
sb.AppendLine($"{DateTime.Now.ToString("HH:mm:ss:fff")}.db初始化");
//新增数据
var dataToAdd = db.Info.Where(x => !db.Info2.Select(y => y.IdentNo).Contains(x.IdentNo))
.ToList();
sb.AppendLine($"{DateTime.Now.ToString("HH:mm:ss:fff")}.获取待新增数据{dataToAdd.Count}条");
dataToAdd.ForEach(x =>
{
var info2 = new Info2
{
IdentNo = x.IdentNo,
Name = x.Name,
Hobby = x.Hobby,
CreateTime = DateTime.Now
};
db.Info2.Add(info2);
});
//更新原有数据
var dataToEdit = db.Info.AsQueryable().Join(db.Info2.AsQueryable(), m => m.IdentNo, n => n.IdentNo,
(m, n) => new
{
info = m,
info2 = n
})
.ToList();
sb.AppendLine($"{DateTime.Now.ToString("HH:mm:ss:fff")}获取待更新数据{dataToEdit.Count}条");
dataToEdit.ForEach(x =>
{
x.info2.Name = x.info.Name;
x.info2.Hobby = x.info.Hobby;
x.info2.UpdateTime = DateTime.Now;
});
sb.AppendLine($"{DateTime.Now.ToString("HH:mm:ss:fff")}开始休眠5s");
Thread.Sleep();
sb.AppendLine($"{DateTime.Now.ToString("HH:mm:ss:fff")}dbSaveBegin");
db.SaveChanges();
sb.AppendLine($"{DateTime.Now.ToString("HH:mm:ss:fff")}dbSaveEnd");
return sb.ToString();
}
里边的这几行代码就是问题重现的重点了:
sb.AppendLine($"{DateTime.Now.ToString("HH:mm:ss:fff")}开始休眠5s");
Thread.Sleep();
sb.AppendLine($"{DateTime.Now.ToString("HH:mm:ss:fff")}dbSaveBegin");
db.SaveChanges();
sb.AppendLine($"{DateTime.Now.ToString("HH:mm:ss:fff")}dbSaveEnd");
在db提交之前,我们让此线程休眠了5s,以模仿现实中一些耗时的操作。这5s期间可能有后续的N个新请求线程进入此函数,那么这些线程获取的数据都会是同样的旧数据了。第一个执行完函数的线程,提交了更改,而后续这些线程再提交的更改,便是基于旧的数据做的更改了。
下面开始重新问题,我们执行一次请求,两表数据情况分别如下:


然后,Info表中新增了一条数据:

这时,我们再执行请求,预期结果应该将行003老虎的数据添加到Info2中,但是我们现在模拟并发,连续调用2次请求看看结果:

可以看到,居然将003老虎的数据插入了两次。这就是并发带来的副作用了。
附上两次请求的辅助输出信息:
//Request1
:::.线程Id:
:::.db初始化
:::.获取待新增数据1条
:::.获取待更新数据2条
:::.开始休眠5s
:::.dbSaveBegin
:::.dbSaveEnd
//Request2
:::.线程Id:
:::.db初始化
:::.获取待新增数据1条
:::287.获取待更新数据2条
:::287.开始休眠5s
:::287.dbSaveBegin
:::339.dbSaveEnd
三、问题解决
既然问题是并发请求导致的,而这个功能不是需要频繁调用的功能,最简便的解决方法就是,我们可以设置此功能同一时间只能由一个线程来访问,即通过lock()的方式。
最终实现代码如下:
public class InfoSync
{
private static object syncObject = new object();
public static string Sync()
{
lock (syncObject)
{
return SyncWork();
}
}
private static string SyncWork()
{
//...
}
}
同时贴出示例控制器的简单实现:
public class DataController : Controller
{
// GET: Data
public ActionResult Index()
{
try
{
var str = InfoSync.Sync();
return Content(str);
}
catch (Exception ex)
{
return Content($"程序发生错误:{ex.Message}\n内部错误:{ex.InnerException.Message}");
}
}
}
四、总结
类似文中数据同步并发情况的实际应用情况还有很多,比如系统有时会需要产生编号,我们会访问数据库中这类编号的最新值,然后计算出下一个编号值,如果不处理并发情况,业务量大时可能就会出现重复编号了。
本文中,针对这类请求并发问题,通过代码锁的方式,将特定功能的并发请求执行转化为队列请求执行,从而避免了问题的发生。
当然,处理并发还有其它途径,如通过数据库锁的方式,再如分布式部署情况下,我们用代码锁的方式也会失效了,实际工作中还需要根据具体情况采用最小代价成本的处理方式。
--End
Asp.net并发请求导致的数据重复插入问题的更多相关文章
- 双重检查加锁机制(并发insert情况下数据重复插入问题的解决方案)
双重检查加锁机制(并发insert情况下数据重复插入问题的解决方案) c#中单例模式和双重检查锁 转:https://blog.csdn.net/zhongliangtang/article/deta ...
- 并发insert情况下数据重复插入问题的解决方案
背景介绍 通常我们在接口里要保存一条数据时,会先判断该条记录在数据库里是否存在,如果不存在就插入,如果存在就返回已经存在. 就拿常见的工单来举例 Order order = orderService. ...
- 关于web资金系统提现安全保护,防止极快的重复并发请求导致重复提现的解决思路
关于WEB金融系统中的提现安全问题很多人没有深入思想,导致有漏洞,常常会遇到有些人遇到被攻击到导资金损失的麻烦, 其实要彻底解决重复并发请求 导致重复提现问题,是需要花点心思的,并没有看起来的 ...
- ASP模拟POST请求异步提交数据的方法
这篇文章主要介绍了ASP模拟POST请求异步提交数据的方法,本文使用MSXML2.SERVERXMLHTTP.3.0实现POST请求,需要的朋友可以参考下 有时需要获取远程网站的某些信息,而服务器又限 ...
- mysql创建唯一索引,避免数据重复插入
多台服务器使用一个数据库时,有时就会出现重复插入的情况,eg:people表中的姓名和身份证号 此时可以给姓名和身份证号创建唯一索引, 创建语句:alter table people add uniq ...
- 关于.Net Core 前后端分离跨域请求时 ajax并发请求导致部分无法通过验证解决办法。
项目中有这样一个页面.页面加载的时候会同时并发6个ajax请求去后端请求下拉框. 这样会导致每次都有1~2个“浏览器预请求”不通过. 浏览器为什么会自动发送“预请求”?请看以面连接 https://b ...
- InnerJoin分页导致的数据重复问题排查
2016年8月9号美好的七夕的早上,我精神抖擞地来到公司.一会之后,客服宅宅MM微信我,说一个VIP大店铺订单导出报表中一个订单有重复行.于是,我赶紧开始查探问题所在.经过一天的反复仔细追查(当然还包 ...
- 如何解决异步接口请求快慢不均导致的数据错误问题? - DevUI
DevUI 是一款面向企业中后台产品的开源前端解决方案,它倡导沉浸.灵活.至简的设计价值观,提倡设计者为真实的需求服务,为多数人的设计,拒绝哗众取宠.取悦眼球的设计.如果你正在开发 ToB 的工具类产 ...
- axios多并发请求
场景: 点击导出Excel按钮实现,姓名列表中前五个的所有的文章数据发送给后端,姓名列表中点击过的数据会被存放到localStorage中: 思路: 点击导出按钮,把前五个数据逐个和localStor ...
随机推荐
- vscode笔记(一)- vscode自动生成文件头部注释和函数注释
VsCode 自动生成文件头部注释和函数注释 作者:狐狸家的鱼 本文链接:vscode自动生成文件头部注释和函数注释 GitHub:sueRimn 1.安装插件KoroFileHeader 2.设置 ...
- Python机器学习第一章
1. 机器学习 (Machine Learning, ML) 1.1 概念:多领域交叉学科,涉及概率论.统计学.逼近论.凸分析.算法复杂度理论等多门学科.专门研究计算机怎样模拟或 ...
- python第九天
复习内容: 文件处理 1. 操作文件的三步骤: ---打开文件:硬盘的空间被操作系统持有 | 文件对象被应用程序持有 ---操作文件:读写操作 ---释放文件:释放操作系统对硬盘空间的持有 2. ...
- Kafka运行一段时间报错Too many open files
Kafka运行一段时间报错Too many open files 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.kafka运行一段时间报错 1>.我最近把kafka集群 ...
- 重装了Devexpress后项目报Dll引用找不到问题解决办法
最近将我的开发环境从VS2015升级到VS2017,升级完后报如下错误,找不到Dev的引用,明明是重新装了dev为什么找不到呢? 经过查看dll引用地址,发现我的dev一开始是安装在C盘,dll引用路 ...
- checkbox 实现互斥选择
// mutex 互斥 checkbox 互斥/** 互斥的原理.找到需要互斥的所有的元素.赋值 checked=false; 然后单独赋值 checked=true* */var mutexbox ...
- shell 生成任意大小文件
$ dd if=/dev/zero of=junk.data bs=1M count=1 参数: if (input file) of (output file) bs(block size) co ...
- 执行python文件
方法一: 通过控制台程序,一行一行执行python程序,输一行执行一行 方法二: 将程序用编辑器编辑好,再在控制台中用路径打开一次性执行(前提是py程序的默认打开方式是python解释器)
- R语言数据集的技术
特征值选择技术要点 特征值选择技术要点(特征值分解) 作者:王立敏 文章来源:xiahouzuoxin 一.特征值分解 1.特征值分解 线性代数中,特征分解(Eigendecomposition),又 ...
- important的妙用解决firefox和ie的css兼容问题
设置css的min-height属性.min-height在Firefox里有效,但IE无法识别.下面有个不错的解决方案,大家可以参考下 对于某些内容可变的层(比如用户评论),我们希望它有个最小的高度 ...