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

一、情景分析

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. noi.ac89A 电梯

    题目 思路 首先按照\(t\)排序!!!! 首先考虑一个暴力\(dp\) 用\(f[i]\)表示前\(i\)个人到达地点所需要的时间. 那么就有如下的转移 \[f_i = min_{1 \le j \ ...

  2. maven settings.xml--需要保存到用户/.m2文件夹下

    <?xml version="1.0" encoding="UTF-8"?> <!-- Licensed to the Apache Soft ...

  3. sourceinsight问题

    1.添加.cc文件 Options-〉“Document   Options”-〉在“File   filter”   裡邊加上*.cc Project--->Add and Remove Pr ...

  4. Grains 与 Pillars

    Grains 与 Pillars Grains介绍 Grains接口是salt用来采集底层系统信息的,包含了操作系统信息.域名.IP地址.内核.内存等一些底层信息.就是因为grains采集了这些信息, ...

  5. python模拟蒙特·卡罗法计算圆周率

    蒙特·卡罗方法是一种通过概率来得到问题近似解的方法,在很多领域都有重要的应用,其中就包括圆周率近似值的计问题. 假设有一块边长为2的正方形木板,上面画一个单位圆,然后随意往木板上扔飞镖,落点坐标(x, ...

  6. SpringBoot整合阿里云OSS文件上传、下载、查看、删除

    1. 开发前准备 1.1 前置知识 java基础以及SpringBoot简单基础知识即可. 1.2 环境参数 开发工具:IDEA 基础环境:Maven+JDK8 所用技术:SpringBoot.lom ...

  7. FPGA

    FPGA(Field-Programmable Gate Array),即现场可编程门阵列,它是在PAL.GAL.CPLD等可编程器件的基础上进一步发展的产物.它是作为专用集成电路(ASIC)领域中的 ...

  8. go语言圣经练习

    练习 3.10: 编写一个非递归版本的comma函数,使用bytes.Buffer代替字符串链接操作. package main import ( "fmt" "os&q ...

  9. SVN的安装与使用教程

    转载:http://www.cnblogs.com/armyfai/p/3985660.html SVN简介: 为什么要使用SVN? 程序员在编写程序的过程中,每个程序员都会生成很多不同的版本,这就需 ...

  10. 关于C的全局变量

    一般来说,不会将全局变量的定义写在头文件中.因为如果多个C源文件都添加了头文件,很容易引起重复定义的问题,这时候一般编译器都会提示.正确的做法是在C源文件中定义一个全局变量,在头文件中加入全局变量的声 ...