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

一、情景分析

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. Curl请求慢

    背景原因:测试环境发现一个连接内网访问和外网访问延迟差别很大,内网访问很快.外网访问很慢.于是我们用curl来诊断问题所在的区域! 命令如下: 1 curl -o /dev/null -s -w %{ ...

  2. pwn-ROP(2)

    通过int80系统只对静态编译有效,动态编译需要用其他方法 本题提供了一个地址输入端,输入函数地址会返回该函数的实际地址,我们用得到的实际地址-偏移地址=基地址,然后用基地址+任意函数的偏移地址就可以 ...

  3. GWAS研究中case和control的比例是有讲究的?

    GWAS研究中,表型分两种.第一种是线性的表型,如果身高.体重.智力等:第二种是二元的表型,比如患病和未患病,即通常所说的case和control.对于表型是线性的样本来说,是不存在case和cont ...

  4. JS基础知识(未完)

    学习笔记,知识点来源于掘金小册-yck-前端面试之道 1.原始类型(6种) 原始类型储存的都是值 number boolean string null undefined symbol null不是对 ...

  5. CodeForces - 314C Sereja and Subsequences (树状数组+dp)

    Sereja has a sequence that consists of n positive integers, a1, a2, ..., an. First Sereja took a pie ...

  6. 010 socket定义服务器

    using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Ne ...

  7. font-family

    Font-family: Helvetica, Tahoma, Arial, “Microsoft YaHei”, “微软雅黑”, SimSun, “宋体”, STXihei, “华文细黑”, Hei ...

  8. centos环境下安装redis

    1.安装gcc和gcc-c++ 使用which gcc 和which g++检查是否已经安装,若已有安装,跳过下面安装步骤 安装gcc yum -y install gcc 安装gcc-c++ yum ...

  9. MySql 从SQL文件导入

    1. 运行cmd进入命令模式,进入Mysql安装目录下的bin目录(即mysql.exe所在的目录): cd c:\"program Files"\MySQL\"MySQ ...

  10. Springboot项目打包后的页面丢失问题(thymeleaf报错)

    遇到的问题目前找到两种: 返回视图路径以/开头,例如 /test/hello 在thymeleaf页面中,引入的页面以/开头,例如:<footer th:replace="/index ...