EF Core下利用Mysql进行数据存储在并发访问下的数据同步问题

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 小故事 在开始讲这篇文章之前,我们来说一个小故事,纯素虚构(真实的存钱逻辑并非如此) 小刘发工资后,赶忙拿着现金去银行,准备把钱存起来,而与此同时,小刘的老婆刘嫂知道小刘的品性,知道他发工资的日子,也知道他喜欢一发工资就去银行存起来,担心小刘卡里存的钱太多拿去“大宝剑”,于是,也去了银行,想趁着小刘把钱存进去后就把钱给取出来,省的夜长梦多。

 

小故事

在开始讲这篇文章之前,我们来说一个小故事,纯素虚构(真实的存钱逻辑并非如此)

小刘发工资后,赶忙拿着现金去银行,准备把钱存起来,而与此同时,小刘的老婆刘嫂知道小刘的品性,知道他发工资的日子,也知道他喜欢一发工资就去银行存起来,担心小刘卡里存的钱太多拿去“大宝剑”,于是,也去了银行,想趁着小刘把钱存进去后就把钱给取出来,省的夜长梦多。

 

小刘与刘嫂取得是两家不同的银行的ATM,所以两人没有碰面。

小刘插入银行卡存钱之前查询了自己的余额,ATM这样显示的:

 

与次同时,刘嫂也通过卡号和密码查询该卡内的余额,也是这么显示的:

 

刘嫂,很生气,没想到小刘偷偷藏了5000块钱的私房钱,就把5000块钱全部取出来了。所以把账户6217****888888的金额更新成0.(查询结果5000基础上减5000)

在这之后,小刘把自己发的3000块钱也存到了银行卡里,所以这边的这台ATM把账户6217****888888的金额更新成了8000.(在查询的5000基础上加3000)

最终的结果是,小刘的银行卡金额8000块钱,刘嫂也拿到了5000块钱。

反思?

故事结束了,很多同学肯定会说,要真有这样的银行不早就倒闭了?确实,真是的银行不可能是这样来计算的,可是我们的同学在设计程序的时候,却经常是这样的一个思路,先从数据库中取值,然后在取到的值的基础上对该值进行修改。可是,却有可能在取到值之后,另外一个客户也取了值,并在你保存之前对数据进行了更新。那么如何解决?

解决办法—乐观锁

常用的办法是,使用客观锁,那么什么是乐观锁?

下面是来自百度百科关于乐观锁的解释:

乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

 

通俗地讲,就是在我们设计数据库的时候,给实体添加一个Version的属性,对实体进行修改前,比较该实体现在的Version和自己当年取出来的Version是否一致,如果一致,对该实体修改,同时,对Version属性+1;如果不一致,则不修改并触发异常。

 

作为强大的EF(Entiry FrameWork)当然对这种操作进行了封装,不用我们自己独立地去实现,但是在查询微软官方文档时,我们发现,官方文档是利用给Sql Server数据库添加timestamp标签实现的,Sql Server在数据发生更改时,能自动地对timestamp进行更新,但是Mysql没有这样的功能的,我是通过并发令牌(ConcurrencyToken)实现的。

 

什么是并发令牌(ConcurrencyToken)?

所谓的并发令牌,就是在实体的属性中添加一块令牌,当对数据执行修改操作时,系统会在Sql语句后加一个Where条件,筛选被标记成令牌的字段是否与取出来一致,如果不一致了,返回的肯定是影响0行,那么此时,就会对抛出异常。

具体怎么用?

首先,新建一个WebApi项目,然后在该项目的Model目录(如果没有就手动创建)新建一个student实体。其代码如下:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Threading.Tasks;
 5 
 6 namespace Bingfa.Model
 7 {
 8     public class Student
 9     {
10         public int id { get; set; }
11         public string Name { get; set; }
12         public string Pwd { get; set; }
13         public int Age { get; set; }
14         public DateTime LastChanged { get; set; }
15     }
16 }

然后创建一个数据库上下文,其代码如下:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.ComponentModel.DataAnnotations.Schema;
 4 using System.Linq;
 5 using System.Threading.Tasks;
 6 using Microsoft.EntityFrameworkCore;
 7 
 8 namespace Bingfa.Model
 9 {
10     public class SchoolContext : DbContext
11     {
12         public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
13         {
14 
15         }
16 
17         public DbSet<Student> students { get; set; }
18 
19         protected override void OnModelCreating(ModelBuilder modelBuilder)
20         {
21             modelBuilder.Entity<Student>().Property(p => p.LastChanged).IsConcurrencyToken() ;
22         }
23     }
24 }

红色部分,我们把Student的LastChange属性标记成并发令牌。

然后在依赖项中选择Nuget包管理器,安装  Pomelo.EntityFrameworkCore.MySql 改引用,该引用可以理解为Mysql的EF Core驱动。

安装成功后,在appsettings.json文件中写入Mysql数据库的连接字符串。写入后,该文件如下:其中红色部分为连接字符串

 1 {
 2   "Logging": {
 3     "IncludeScopes": false,
 4     "Debug": {
 5       "LogLevel": {
 6         "Default": "Warning"
 7       }
 8     },
 9     "Console": {
10       "LogLevel": {
11         "Default": "Warning"
12       }
13     }
14   },
15   "ConnectionStrings": { "Connection": "Data Source=127.0.0.1;Database=school;User ID=root;Password=123456;pooling=true;CharSet=utf8;port=3306;" }
16 }

然后,在Stutup.cs中对Mysql进行依赖注入:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Threading.Tasks;
 5 using Bingfa.Model;
 6 using Microsoft.AspNetCore.Builder;
 7 using Microsoft.AspNetCore.Hosting;
 8 using Microsoft.EntityFrameworkCore;
 9 using Microsoft.Extensions.Configuration;
10 using Microsoft.Extensions.DependencyInjection;
11 using Microsoft.Extensions.Logging;
12 using Microsoft.Extensions.Options;
13 
14 namespace Bingfa
15 {
16     public class Startup
17     {
18         public Startup(IConfiguration configuration)
19         {
20             Configuration = configuration;
21         }
22 
23         public IConfiguration Configuration { get; }
24 
25         // This method gets called by the runtime. Use this method to add services to the container.
26         public void ConfigureServices(IServiceCollection services)
27         {
28             var connection = Configuration.GetConnectionString("Connection");
29             services.AddDbContext<SchoolContext>(options =>
30             {
31                 options.UseMySql(connection);
32                 options.UseLoggerFactory(new LoggerFactory().AddConsole());
33             });
34             services.AddMvc();
35         }
36 
37         // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
38         public void Configure(IApplicationBuilder app, IHostingEnvironment env)
39         {
40             if (env.IsDevelopment())
41             {
42                 app.UseDeveloperExceptionPage();
43             }
44 
45             app.UseMvc();
46         }
47     }
48 }

其中,红色字体部分即为对Mysql数据库上下文进行注入,蓝色背景部分,为将sql语句在控制台中输出,便于我们查看运行过程中的sql语句。

以上操作完成后,即可在数据库中生成表了。打开程序包管理控制台,打开方式如下:

打开后分别输入以下两条命令:、

add-migration init

update-database

是分别输入哦,不是一次输入两条,语句执行效果如图:

执行完成后即可在Mysql数据库中看到生成的数据表了,如图。

最后,我们就要进行实际的业务处理过程的编码了。打开ValuesController.cs的代码,我修改后代码如下

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Threading.Tasks;
 5 using Bingfa.Model;
 6 using Microsoft.AspNetCore.Mvc;
 7 
 8 namespace Bingfa.Controllers
 9 {
10     [Route("api/[controller]")]
11     public class ValuesController : Controller
12     {
13         private SchoolContext schoolContext;
14 
15         public ValuesController(SchoolContext _schoolContext)//控制反转,依赖注入
16         {
17             schoolContext = _schoolContext;
18         }
19         
20         // GET api/values/5
21         [HttpGet("{id}")]
22         public Student Get(int id)
23         {
24             return schoolContext.students.Where(p => p.id == id).FirstOrDefault();  //通过Id获取学生数据
25         }
26         [HttpGet]
27         public List<Student> Get()
28         {
29             return schoolContext.students.ToList();   //获取所有的学生数据
30         }
31 
32         // POST api/values
33         [HttpPost]
34         public string Post(Student student)   //更新学生数据
35         {
36             if (student.id != 0)
37             {
38                 try
39                 {
40                     Student studentDataBase = schoolContext.students.Where(p => p.id == student.id).FirstOrDefault();   //首先通过Id找到该学生
41 
42                     //如果查找到的学生的LastChanged与Post过来的数据的LastChanged的时间相同,则表示数据没有修改过
43                     //为了控制时间精度,对时间进行秒后取三位小数
44                     if (studentDataBase.LastChanged.ToString("yyyy-MM-dd HH:mm:ss.fff").Equals(student.LastChanged.ToString("yyyy-MM-dd HH:mm:ss.fff")))
45                     {
46                         studentDataBase.LastChanged=DateTime.Now;//把数据的LastChanged更改成现在的时间
47                         studentDataBase.Age = student.Age;
48                         studentDataBase.Name = student.Name;
49                         studentDataBase.Pwd = student.Pwd;
50                         schoolContext.SaveChanges();  //保存数据
51                     }
52                     else
53                     {
54                         throw new Exception("数据已经修改,请刷新查看");
55                         //return "";
56                     }
57                 }
58                 catch (Exception e)
59                 {
60                     return e.Message;
61                 }
62                 return "success";
63             }
64             return "没有找到该Student";
65         }
66 
67         // PUT api/values/5
68         [HttpPut("{id}")]
69         public void Put(int id, [FromBody]string value)
70         {
71 
72         }
73 
74         // DELETE api/values/5
75         [HttpDelete("{id}")]
76         public void Delete(int id)
77         {
78         }
79     }
80 }

主要代码在Post方法中。

为了方便看到运行的Sql语句,我们需要把启动程序更改成项目本身而不是IIS。如图

启动后效果如图:

我们先往数据库中插入一条数据

然后,通过访问http://localhost:56295/api/values/1即可获取该条数据,如图:

我们把该数据修改age成2之后,利用postMan把数据post到控制器,进行数据修改,如图,修改成功

那么,我们把age修改成3,LastChange的数据依然用第一次获取到的时间进行Post,那么返回的结果如图:

可以看到,执行了catch内的代码,触发了异常,没有接受新的提交。

最后,我们看看加了并发锁之后的sql语句:

从控制台中输出的sql语句可以看到  对LastChanged属性进行了筛选,只有当LastChanged与取出该实体时一致,该更新才会执行。

这就是乐观锁的实现过程。

并发访问测试程序

为了对该程序进行测试,我特意编写了一个程序,多线程地对数据库的数据进行get和post,模拟一个并发访问的过程,代码如下:

 1 using System;
 2 using System.Net;
 3 using System.Net.Http;
 4 using System.Threading;
 5 using Newtonsoft.Json;
 6 
 7 namespace Test
 8 {
 9     class Program
10     {
11         static void Main(string[] args)
12         {
13             Console.WriteLine("输入回车开始测试...");
14             Console.ReadKey();
15             ServicePointManager.DefaultConnectionLimit = 1000;
16             for (int i = 0; i < 10; i++)
17             {
18                 Thread td = new Thread(new ParameterizedThreadStart(PostTest));
19                 td.Start(i);
20                 Thread.Sleep(new Random().Next(1,100));//随机休眠时长
21             }
22             Console.ReadLine();
23         }
24         public static void PostTest(object i)
25         {
26             try
27             {
28                 string url = "http://localhost:56295/api/values/1";//获取ID为1的student的信息
29                 Student student = JsonConvert.DeserializeObject<Student>(RequestHandler.HttpGet(url));
30                 student.Age++;//对年龄进行修改
31                 string postData = $"Id={ student.id}&age={student.Age}&Name={student.Name}&Pwd={student.Pwd}&LastChanged={student.LastChanged.ToString("yyyy-MM-dd HH:mm:ss.fff")}";
32                 Console.WriteLine($"线程{i.ToString()}Post数据{postData}");
33                 string r = RequestHandler.HttpPost("http://localhost:56295/api/values", postData);
34                 Console.WriteLine($"线程{i.ToString()}Post结果{r}");
35             }
36             catch (Exception ex)
37             {
38                 Console.WriteLine(ex.Message);
39             }
40 
41         }
42     }
43 }

测试效果:

可以看到,部分修改成功了,部分没有修改成功,这就是乐观锁的效果。

项目的完整代码我已经提交到github,有兴趣的可以访问以下地址查看:

https://github.com/liuzhenyulive/Bingfa

第一次这么认真地写一篇文章,如果喜欢,请推荐支持,谢谢!

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
4月前
|
SQL DataWorks 关系型数据库
DataWorks操作报错合集之如何处理数据同步时(mysql->hive)报:Render instance failed
DataWorks是阿里云提供的一站式大数据开发与治理平台,支持数据集成、数据开发、数据服务、数据质量管理、数据安全管理等全流程数据处理。在使用DataWorks过程中,可能会遇到各种操作报错。以下是一些常见的报错情况及其可能的原因和解决方法。
|
2月前
|
监控 关系型数据库 MySQL
深入了解MySQL主从复制:构建高效稳定的数据同步架构
深入了解MySQL主从复制:构建高效稳定的数据同步架构
135 1
|
3月前
|
canal 消息中间件 关系型数据库
Canal作为一款高效、可靠的数据同步工具,凭借其基于MySQL binlog的增量同步机制,在数据同步领域展现了强大的应用价值
【9月更文挑战第1天】Canal作为一款高效、可靠的数据同步工具,凭借其基于MySQL binlog的增量同步机制,在数据同步领域展现了强大的应用价值
809 4
|
4月前
|
SQL DataWorks 关系型数据库
DataWorks产品使用合集之如何跨账号访问同一个MySQL
DataWorks作为一站式的数据开发与治理平台,提供了从数据采集、清洗、开发、调度、服务化、质量监控到安全管理的全套解决方案,帮助企业构建高效、规范、安全的大数据处理体系。以下是对DataWorks产品使用合集的概述,涵盖数据处理的各个环节。
|
4月前
|
关系型数据库 MySQL 数据库
【MySQL】手把手教你MySQL数据同步
【MySQL】手把手教你MySQL数据同步
|
2月前
|
关系型数据库 MySQL Linux
Docker安装Mysql5.7,解决无法访问DockerHub问题
当 Docker Hub 无法访问时,可以通过配置国内镜像加速来解决应用安装失败和镜像拉取超时的问题。本文介绍了如何在 CentOS 上一键配置国内镜像加速,并成功拉取 MySQL 5.7 镜像。
567 2
Docker安装Mysql5.7,解决无法访问DockerHub问题
|
1月前
|
JSON 关系型数据库 MySQL
MySQL JSON数据存储结构与操作
通过本文的介绍,我们了解了MySQL中JSON数据类型的基本操作、常用JSON函数、以及如何通过索引和优化来提高查询性能。JSON数据类型为存储和操作结构化数据提供了灵活性和便利性,在现代数据库应用中具有广泛的应用前景。希望本文对您在MySQL中使用JSON数据类型有所帮助。
91 0
|
3月前
|
存储 关系型数据库 MySQL
深入解析MySQL数据存储机制:从表结构到物理存储
深入解析MySQL数据存储机制:从表结构到物理存储
320 1
|
2月前
|
消息中间件 NoSQL 关系型数据库
一文彻底搞定Redis与MySQL的数据同步
【10月更文挑战第21天】本文介绍了 Redis 与 MySQL 数据同步的原因及实现方式。同步的主要目的是为了优化性能和保持数据一致性。实现方式包括基于数据库触发器、应用层双写和使用消息队列。每种方式都有其优缺点,需根据具体场景选择合适的方法。此外,文章还强调了数据同步时需要注意的数据一致性、性能优化和异常处理等问题。
664 0
|
4月前
|
canal 关系型数据库 MySQL
"揭秘阿里数据同步黑科技Canal:从原理到实战,手把手教你玩转MySQL数据秒级同步,让你的数据处理能力瞬间飙升,成为技术界的新晋网红!"
【8月更文挑战第18天】Canal是一款由阿里巴巴开源的高性能数据同步系统,它通过解析MySQL的增量日志(Binlog),提供低延迟、可靠的数据订阅和消费功能。Canal模拟MySQL Slave与Master间的交互协议来接收并解析Binary Log,支持数据的增量同步。配置简单直观,包括Server和Instance两层配置。在实战中,Canal可用于数据库镜像、实时备份等多种场景,通过集成Canal Client可实现数据的消费和处理,如更新缓存或写入消息队列。
863 0