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

一、情景分析

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. js-sha256源码

    /** * [js-sha256]{@link https://github.com/emn178/js-sha256} * * @version 0.9.0 * @author Chen, Yi-C ...

  2. Linux路径与Win路径的转换

    cygpath $ cygpath -p "$WinPath" -u LinuxPath $ cygpath -p "$LinuxPath" -w WinPat ...

  3. time series analysis

    1 总体介绍 在以下主题中,我们将回顾有助于分析时间序列数据的技术,即遵循非随机顺序的测量序列.与在大多数其他统计数据的上下文中讨论的随机观测样本的分析不同,时间序列的分析基于数据文件中的连续值表示以 ...

  4. cookie 和 session 的异同

    cookie和session机制是web中常用的跟踪技术,用来跟踪用户的整个会话.cookie通过在客户端记录信息确定用户的身份,session通过在服务器端记录信息确定用户身份. (1)cookie ...

  5. 老男孩Python全栈学习 S9 日常作业 012

    1.斐波那契数列用递归实现:问第n个斐波那契数是多少 def fbnq(n): if n == 0 or n == 1: return 1 else: return fbnq(n-1)+fbnq(n- ...

  6. nginx配置vue项目部署访问无问题,刷新出现404问题

    现象: 在浏览器中直接访问www.test.com/api1/login会404.但如果你先访问www.test.com后再点“登录" 跳转到www.test.com/api1/login是 ...

  7. 部署kubernetes-dashboard

    1.生成访问证书 | awk '{print $2}' | base64 -d >> kubecfg.crt | awk '{print $2}' | base64 -d >> ...

  8. AFM论文精读

    深度学习在推荐系统的应用(二)中AFM的简单回顾 AFM模型(Attentional Factorization Machine) 模型原始论文 Attentional Factorization M ...

  9. 《Java》第八周学习总结

    第八周学习内容:课本第15章节的内容泛型与集合框架  主要内容有 -泛型-链表-堆栈-散列映射-树集-树映射 重点和难点-重点:泛型和集合的使用码云:https://gitee.com/ShengHu ...

  10. IPv6 RIPng (PT)

    在Cisco路由器上配置RIPng  一.拓扑图 二.地址表 Device Interface IPv6 Address R1 F 0/0 2001:0DB8:CAFE:0001::1/64 S 0/ ...