• 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表。

    表实体代码实现如下:

     1     /// <summary>
     2     /// 信息表
     3     /// </summary>
     4     public class Info
     5     {
     6         public int Id { get; set; }
     7         /// <summary>
     8         /// 证件号码
     9         /// </summary>
    10         public string IdentNo { get; set; }
    11         /// <summary>
    12         /// 姓名
    13         /// </summary>
    14         public string Name { get; set; }
    15         /// <summary>
    16         /// 爱好
    17         /// </summary>
    18         public string Hobby { get; set; }
    19         /// <summary>
    20         /// 备注信息
    21         /// </summary>
    22         public string Bz { get; set; }
    23     }
    24 
    25     /// <summary>
    26     /// 信息表2
    27     /// </summary>
    28     public class Info2
    29     {
    30         public int Id { get; set; }
    31         /// <summary>
    32         /// 证件号码
    33         /// </summary>
    34         public string IdentNo { get; set; }
    35         /// <summary>
    36         /// 姓名
    37         /// </summary>
    38         public string Name { get; set; }
    39         /// <summary>
    40         /// 爱好
    41         /// </summary>
    42         public string Hobby { get; set; }
    43         /// <summary>
    44         /// 创建时间
    45         /// </summary>
    46         public DateTime CreateTime { get; set; }
    47         /// <summary>
    48         /// 最后修改时间
    49         /// </summary>
    50         public DateTime? UpdateTime { get; set; }
    51         /// <summary>
    52         /// 评分
    53         /// </summary>
    54         public int? Score { get; set; }
    55     }
    View Code

     SyncWork()代码实现如下,代码中加入了辅助的输出信息:

     1         public static string SyncWork()
     2         {
     3             StringBuilder sb = new StringBuilder();
     4             //
     5             int threadId = Thread.CurrentThread.ManagedThreadId;
     6             sb.AppendLine($"{DateTime.Now.ToString("HH:mm:ss:fff")}.线程Id:{threadId}");
     7             //
     8             MyDbContext db = new MyDbContext();
     9             sb.AppendLine($"{DateTime.Now.ToString("HH:mm:ss:fff")}.db初始化");
    10             //新增数据
    11             var dataToAdd = db.Info.Where(x => !db.Info2.Select(y => y.IdentNo).Contains(x.IdentNo))
    12                 .ToList();
    13             sb.AppendLine($"{DateTime.Now.ToString("HH:mm:ss:fff")}.获取待新增数据{dataToAdd.Count}条");
    14             dataToAdd.ForEach(x =>
    15             {
    16                 var info2 = new Info2
    17                 {
    18                     IdentNo = x.IdentNo,
    19                     Name = x.Name,
    20                     Hobby = x.Hobby,
    21                     CreateTime = DateTime.Now
    22                 };
    23                 db.Info2.Add(info2);
    24             });
    25             //更新原有数据
    26             var dataToEdit = db.Info.AsQueryable().Join(db.Info2.AsQueryable(), m => m.IdentNo, n => n.IdentNo, 
    27                 (m, n) =>  new
    28                 {
    29                     info = m,
    30                     info2 = n
    31                 })
    32                 .ToList();
    33             sb.AppendLine($"{DateTime.Now.ToString("HH:mm:ss:fff")}获取待更新数据{dataToEdit.Count}条");
    34             dataToEdit.ForEach(x =>
    35             {
    36                 x.info2.Name = x.info.Name;
    37                 x.info2.Hobby = x.info.Hobby;
    38                 x.info2.UpdateTime = DateTime.Now;
    39             });
    40             sb.AppendLine($"{DateTime.Now.ToString("HH:mm:ss:fff")}开始休眠5s");
    41             Thread.Sleep(5000);
    42             sb.AppendLine($"{DateTime.Now.ToString("HH:mm:ss:fff")}dbSaveBegin");
    43             db.SaveChanges();
    44             sb.AppendLine($"{DateTime.Now.ToString("HH:mm:ss:fff")}dbSaveEnd");
    45             return sb.ToString();
    46         }
    View Code

     里边的这几行代码就是问题重现的重点了:

    1             sb.AppendLine($"{DateTime.Now.ToString("HH:mm:ss:fff")}开始休眠5s");
    2             Thread.Sleep(5000);
    3             sb.AppendLine($"{DateTime.Now.ToString("HH:mm:ss:fff")}dbSaveBegin");
    4             db.SaveChanges();
    5             sb.AppendLine($"{DateTime.Now.ToString("HH:mm:ss:fff")}dbSaveEnd");

    在db提交之前,我们让此线程休眠了5s,以模仿现实中一些耗时的操作。这5s期间可能有后续的N个新请求线程进入此函数,那么这些线程获取的数据都会是同样的旧数据了。第一个执行完函数的线程,提交了更改,而后续这些线程再提交的更改,便是基于旧的数据做的更改了。

    下面开始重新问题,我们执行一次请求,两表数据情况分别如下:

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

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

     

    可以看到,居然将003老虎的数据插入了两次。这就是并发带来的副作用了。

    附上两次请求的辅助输出信息:

     1 //Request1
     2 22:50:05:953.线程Id:44
     3 22:50:05:953.db初始化
     4 22:50:05:982.获取待新增数据1条
     5 22:50:06:000.获取待更新数据2条
     6 22:50:06:001.开始休眠5s
     7 22:50:11:001.dbSaveBegin
     8 22:50:11:084.dbSaveEnd
     9 //Request2
    10 22:50:07:240.线程Id:48
    11 22:50:07:240.db初始化
    12 22:50:07:270.获取待新增数据1条
    13 22:50:07:287.获取待更新数据2条
    14 22:50:07:287.开始休眠5s
    15 22:50:12:287.dbSaveBegin
    16 22:50:12:339.dbSaveEnd

    三、问题解决

    既然问题是并发请求导致的,而这个功能不是需要频繁调用的功能,最简便的解决方法就是,我们可以设置此功能同一时间只能由一个线程来访问,即通过lock()的方式。

    最终实现代码如下:

     1     public class InfoSync
     2     {
     3         private static object syncObject = new object();
     4         public static string Sync()
     5         {
     6             lock (syncObject)
     7             {
     8                 return SyncWork();
     9             }
    10         }
    11         private static string SyncWork()
    12         {
    13             //...  
    14         }
    15     }
    View Code

    同时贴出示例控制器的简单实现:

     1     public class DataController : Controller
     2     {
     3         // GET: Data
     4         public ActionResult Index()
     5         {
     6             try
     7             {
     8                 var str = InfoSync.Sync();
     9                 return Content(str);
    10             }
    11             catch (Exception ex)
    12             {
    13                 return Content($"程序发生错误:{ex.Message}
    内部错误:{ex.InnerException.Message}");
    14             }
    15         }
    16     }
    View Code

    四、总结

    类似文中数据同步并发情况的实际应用情况还有很多,比如系统有时会需要产生编号,我们会访问数据库中这类编号的最新值,然后计算出下一个编号值,如果不处理并发情况,业务量大时可能就会出现重复编号了。

    本文中,针对这类请求并发问题,通过代码锁的方式,将特定功能的并发请求执行转化为队列请求执行,从而避免了问题的发生。

    当然,处理并发还有其它途径,如通过数据库锁的方式,再如分布式部署情况下,我们用代码锁的方式也会失效了,实际工作中还需要根据具体情况采用最小代价成本的处理方式。

  • 相关阅读:
    面向对象与类
    引用数据类型
    方法
    java for 语句的用法
    java 数组
    Scanner与Random
    java基本语法
    java介绍及安装。
    数据库设计
    NFS相关、NFS服务端安装配置、exportfs命令、nfs客户端的问题
  • 原文地址:https://www.cnblogs.com/jiujiduilie/p/10328232.html
Copyright © 2020-2023  润新知