前段时间工作中,有客户反应了系统中某类待办重复出现两次的情况。我核实了数据之后,分析认为是并发请求下导致的数据不一致性问题,并做了重现。其实这并不是一个需要频繁调用的功能,但是客户连续点击了两次,导致出现了并发问题。除了前端优化,这里重点探讨后台方面代码层面的处理,最终解决问题。

一、情景分析

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并发请求导致的数据重复插入问题的更多相关文章

  1. 双重检查加锁机制(并发insert情况下数据重复插入问题的解决方案)

    双重检查加锁机制(并发insert情况下数据重复插入问题的解决方案) c#中单例模式和双重检查锁 转:https://blog.csdn.net/zhongliangtang/article/deta ...

  2. 并发insert情况下数据重复插入问题的解决方案

    背景介绍 通常我们在接口里要保存一条数据时,会先判断该条记录在数据库里是否存在,如果不存在就插入,如果存在就返回已经存在. 就拿常见的工单来举例 Order order = orderService. ...

  3. 关于web资金系统提现安全保护,防止极快的重复并发请求导致重复提现的解决思路

    关于WEB金融系统中的提现安全问题很多人没有深入思想,导致有漏洞,常常会遇到有些人遇到被攻击到导资金损失的麻烦,     其实要彻底解决重复并发请求 导致重复提现问题,是需要花点心思的,并没有看起来的 ...

  4. ASP模拟POST请求异步提交数据的方法

    这篇文章主要介绍了ASP模拟POST请求异步提交数据的方法,本文使用MSXML2.SERVERXMLHTTP.3.0实现POST请求,需要的朋友可以参考下 有时需要获取远程网站的某些信息,而服务器又限 ...

  5. mysql创建唯一索引,避免数据重复插入

    多台服务器使用一个数据库时,有时就会出现重复插入的情况,eg:people表中的姓名和身份证号 此时可以给姓名和身份证号创建唯一索引, 创建语句:alter table people add uniq ...

  6. 关于.Net Core 前后端分离跨域请求时 ajax并发请求导致部分无法通过验证解决办法。

    项目中有这样一个页面.页面加载的时候会同时并发6个ajax请求去后端请求下拉框. 这样会导致每次都有1~2个“浏览器预请求”不通过. 浏览器为什么会自动发送“预请求”?请看以面连接 https://b ...

  7. InnerJoin分页导致的数据重复问题排查

    2016年8月9号美好的七夕的早上,我精神抖擞地来到公司.一会之后,客服宅宅MM微信我,说一个VIP大店铺订单导出报表中一个订单有重复行.于是,我赶紧开始查探问题所在.经过一天的反复仔细追查(当然还包 ...

  8. 如何解决异步接口请求快慢不均导致的数据错误问题? - DevUI

    DevUI 是一款面向企业中后台产品的开源前端解决方案,它倡导沉浸.灵活.至简的设计价值观,提倡设计者为真实的需求服务,为多数人的设计,拒绝哗众取宠.取悦眼球的设计.如果你正在开发 ToB 的工具类产 ...

  9. axios多并发请求

    场景: 点击导出Excel按钮实现,姓名列表中前五个的所有的文章数据发送给后端,姓名列表中点击过的数据会被存放到localStorage中: 思路: 点击导出按钮,把前五个数据逐个和localStor ...

随机推荐

  1. java的集合:List、Set和Map

    虚线是接口,实线是实现类: 集合能够解决的问题:集合可以丽杰为是一种更高级的数组,可以保存多条数据 本质:java官方开发人员基于java的一些基础内容(数组等等)创建了一些接口和类,然后使用这些接口 ...

  2. javaWeb使用百度编辑器上传图片的问题

    1.先看项目结构(访问网站:http://localhost:8080/baidu_edit/) 2.部署6个jar包 1)先将jsp/lib的6个jar包放入到WEB-INF/lib文件夹中 2)然 ...

  3. python类方法以及类调用实例方法的理解

    classmethod类方法 1) 在python中.类方法 @classmethod 是一个函数修饰符,它表示接下来的是一个类方法,而对于平常我们见到的则叫做实例方法. 类方法的第一个参数cls,而 ...

  4. percona-xtrabackup快速安装及其简单使用

    percona-xtrabackup快速安装及其简单使用 cd /opt/环境:centos6.x yum -y install perl-DBIyum -y install perl-DBD-MyS ...

  5. python之路(4)高阶函数和python内置函数

    前言 函数式编程不用变量保存状态,不改变变量 内置函数 高阶函数 把函数当作参数传给另一个对象 返回值中包含函数 使用的场景演示: num_test = [1,2,10,5,8,7] 客户说 :对上述 ...

  6. C51学习

    十六个数字循环显示 #include<reg52.h>#include<intrins.h>#define uint unsigned int#define uchar uns ...

  7. 20155324《网络对抗》Exp1 PC平台逆向破解(5)M

    20155324<网络对抗>Exp1 PC平台逆向破解(5)M 实验目标 本次实践的对象是一个名为~pwn1~的~linux~可执行文件. 该程序正常执行流程是:~main~调用~foo~ ...

  8. 【翻译】 Guice 动机——依赖注入的动机

    原文链接 动机 将所有的内容连接在一起时应用开发的一个单调乏味的部分.有几种方式来将数据.服务.presetntation类连接到一起.为了对比这些方法,我将为披萨订购网站编写账单代码: public ...

  9. JS+CSS实现弹出全屏灰黑色透明遮罩效果的方法

    本文实例讲述了js+CSS实现弹出一个全屏灰黑色透明遮罩效果的方法.分享给大家供大家参考.具体分析如下: 在众多的网站都有这样的效果,当进行一定的操作之后,会弹出一个灰黑色的半透明的遮罩,在上面可以操 ...

  10. day 21 - 1 包,异常处理

    创建目录代码 1. 无论是 import 形式还是 from...import 形式,凡是在导入语句中(而不是在使用时)遇到带点的,都要第一时间提高警觉:这是关于包才有的导入语法2. 包是目录级的(文 ...