EF Core下利用Mysql进行数据存储在并发访问下的数据同步问题
小故事
在开始讲这篇文章之前,我们来说一个小故事,纯素虚构(真实的存钱逻辑并非如此)
小刘发工资后,赶忙拿着现金去银行,准备把钱存起来,而与此同时,小刘的老婆刘嫂知道小刘的品性,知道他发工资的日子,也知道他喜欢一发工资就去银行存起来,担心小刘卡里存的钱太多拿去“大宝剑”,于是,也去了银行,想趁着小刘把钱存进去后就把钱给取出来,省的夜长梦多。
小刘与刘嫂取得是两家不同的银行的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实体。其代码如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; namespace Bingfa.Model
{
public class Student
{
public int id { get; set; }
public string Name { get; set; }
public string Pwd { get; set; }
public int Age { get; set; }
public DateTime LastChanged { get; set; }
}
}
然后创建一个数据库上下文,其代码如下:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore; namespace Bingfa.Model
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{ } public DbSet<Student> students { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>().Property(p => p.LastChanged).IsConcurrencyToken() ;
}
}
}
红色部分,我们把Student的LastChange属性标记成并发令牌。
然后在依赖项中选择Nuget包管理器,安装 Pomelo.EntityFrameworkCore.MySql 改引用,该引用可以理解为Mysql的EF Core驱动。
安装成功后,在appsettings.json文件中写入Mysql数据库的连接字符串。写入后,该文件如下:其中红色部分为连接字符串
{
"Logging": {
"IncludeScopes": false,
"Debug": {
"LogLevel": {
"Default": "Warning"
}
},
"Console": {
"LogLevel": {
"Default": "Warning"
}
}
},
"ConnectionStrings": { "Connection": "Data Source=127.0.0.1;Database=school;User ID=root;Password=123456;pooling=true;CharSet=utf8;port=3306;" }
}
然后,在Stutup.cs中对Mysql进行依赖注入:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Bingfa.Model;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; namespace Bingfa
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
} public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
var connection = Configuration.GetConnectionString("Connection");
29 services.AddDbContext<SchoolContext>(options =>
30 {
31 options.UseMySql(connection);
32 options.UseLoggerFactory(new LoggerFactory().AddConsole());
33 });
services.AddMvc();
} // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
} app.UseMvc();
}
}
}
其中,红色字体部分即为对Mysql数据库上下文进行注入,蓝色背景部分,为将sql语句在控制台中输出,便于我们查看运行过程中的sql语句。
以上操作完成后,即可在数据库中生成表了。打开程序包管理控制台,打开方式如下:

打开后分别输入以下两条命令:、
add-migration init
update-database
是分别输入哦,不是一次输入两条,语句执行效果如图:

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

最后,我们就要进行实际的业务处理过程的编码了。打开ValuesController.cs的代码,我修改后代码如下
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Bingfa.Model;
using Microsoft.AspNetCore.Mvc; namespace Bingfa.Controllers
{
[Route("api/[controller]")]
public class ValuesController : Controller
{
private SchoolContext schoolContext; public ValuesController(SchoolContext _schoolContext)//控制反转,依赖注入
{
schoolContext = _schoolContext;
} // GET api/values/5
[HttpGet("{id}")]
public Student Get(int id)
{
return schoolContext.students.Where(p => p.id == id).FirstOrDefault(); //通过Id获取学生数据
}
[HttpGet]
public List<Student> Get()
{
return schoolContext.students.ToList(); //获取所有的学生数据
} // POST api/values
[HttpPost]
public string Post(Student student) //更新学生数据
{
if (student.id != )
{
try
{
Student studentDataBase = schoolContext.students.Where(p => p.id == student.id).FirstOrDefault(); //首先通过Id找到该学生 //如果查找到的学生的LastChanged与Post过来的数据的LastChanged的时间相同,则表示数据没有修改过
//为了控制时间精度,对时间进行秒后取三位小数
if (studentDataBase.LastChanged.ToString("yyyy-MM-dd HH:mm:ss.fff").Equals(student.LastChanged.ToString("yyyy-MM-dd HH:mm:ss.fff")))
{
studentDataBase.LastChanged=DateTime.Now;//把数据的LastChanged更改成现在的时间
studentDataBase.Age = student.Age;
studentDataBase.Name = student.Name;
studentDataBase.Pwd = student.Pwd;
schoolContext.SaveChanges(); //保存数据
}
else
{
throw new Exception("数据已经修改,请刷新查看");
//return "";
}
}
catch (Exception e)
{
return e.Message;
}
return "success";
}
return "没有找到该Student";
} // PUT api/values/5
[HttpPut("{id}")]
public void Put(int id, [FromBody]string value)
{ } // DELETE api/values/5
[HttpDelete("{id}")]
public void Delete(int id)
{
}
}
}
主要代码在Post方法中。
为了方便看到运行的Sql语句,我们需要把启动程序更改成项目本身而不是IIS。如图

启动后效果如图:

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

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

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

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

可以看到,执行了catch内的代码,触发了异常,没有接受新的提交。
最后,我们看看加了并发锁之后的sql语句:

从控制台中输出的sql语句可以看到 对LastChanged属性进行了筛选,只有当LastChanged与取出该实体时一致,该更新才会执行。
这就是乐观锁的实现过程。
并发访问测试程序
为了对该程序进行测试,我特意编写了一个程序,多线程地对数据库的数据进行get和post,模拟一个并发访问的过程,代码如下:
using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using Newtonsoft.Json; namespace Test
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("输入回车开始测试...");
Console.ReadKey();
ServicePointManager.DefaultConnectionLimit = ;
for (int i = ; i < ; i++)
{
Thread td = new Thread(new ParameterizedThreadStart(PostTest));
td.Start(i);
Thread.Sleep(new Random().Next(,));//随机休眠时长
}
Console.ReadLine();
}
public static void PostTest(object i)
{
try
{
string url = "http://localhost:56295/api/values/1";//获取ID为1的student的信息
Student student = JsonConvert.DeserializeObject<Student>(RequestHandler.HttpGet(url));
student.Age++;//对年龄进行修改
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")}";
Console.WriteLine($"线程{i.ToString()}Post数据{postData}");
string r = RequestHandler.HttpPost("http://localhost:56295/api/values", postData);
Console.WriteLine($"线程{i.ToString()}Post结果{r}");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
} }
}
}
测试效果:

可以看到,部分修改成功了,部分没有修改成功,这就是乐观锁的效果。
项目的完整代码我已经提交到github,有兴趣的可以访问以下地址查看:
https://github.com/liuzhenyulive/Bingfa
第一次这么认真地写一篇文章,如果喜欢,请推荐支持,谢谢!
EF Core下利用Mysql进行数据存储在并发访问下的数据同步问题的更多相关文章
- .NET 5/.NET Core使用EF Core 5连接MySQL数据库写入/读取数据示例教程
本文首发于<.NET 5/.NET Core使用EF Core 5(Entity Framework Core)连接MySQL数据库写入/读取数据示例教程> 前言 在.NET Core/. ...
- [转]高并发访问下避免对象缓存失效引发Dogpile效应
避免Redis/Memcached缓存失效引发Dogpile效应 Redis/Memcached高并发访问下的缓存失效时可能产生Dogpile效应(Cache Stampede效应). 推荐阅读:高并 ...
- Windows下更改MySQL数据库的存储位置
在MySQL安装完成后,要修改数据库存储的位置,比如从安装目录下的C:\Program Files\MySQL\MySQL Server 5.0\Data文件夹转移到D:\mySQLData文件夹. ...
- MySQL在大数据、高并发场景下的SQL语句优化和"最佳实践"
本文主要针对中小型应用或网站,重点探讨日常程序开发中SQL语句的优化问题,所谓“大数据”.“高并发”仅针对中小型应用而言,专业的数据库运维大神请无视.以下实践为个人在实际开发工作中,针对相对“大数据” ...
- c# redis 利用锁(StackExchange.Redis LockTake)来保证数据在高并发情况下的正确性
之前有写过一篇介绍c#操作redis的文章 http://www.cnblogs.com/axel10/p/8459434.html ,这篇文章中的案例使用了StringIncrement来实现了高并 ...
- 安卓android sharepreference数据存储,保存输入框里面的数据
Fragment 里面 使用轻量级的数据存储sharepreference ,代码思路清晰.保存输入框里面的数据,实现按钮保存. 个人项目中简单清晰代码: 赵存档 编写 ,可以参考: 类继承Fragm ...
- 大数据量高并发访问SQL优化方法
保证在实现功能的基础上,尽量减少对数据库的访问次数:通过搜索参数,尽量减少对表的访问行数,最小化结果集,从而减轻网络负担:能够分开的操作尽量分开处理,提高每次的响应速度:在数据窗口使用SQL时,尽量把 ...
- mysql数据库 myisam数据存储引擎 表由于索引和数据导致的表损坏 的修复 和检查
一.mysqlcheck 进行表的检查和修复 1.检查mysqlisam存储引擎表的状态 #mysqlcheck -uuser -ppassword database table -c #检查单 ...
- 使用ef core自动生成mysql表和数据编码的问题
mysql默认的编码是不支持中文的,需要改成utf8编码格式. 而我使用的Pomelo.EntityFrameworkCore.MySql组件生成mysql库和表,他是使用默认编码的. 网上大多说修改 ...
随机推荐
- Python基础篇(三)
元组是序列的一种,与列表的区别是,元组是不能修改的. 元组一般是用圆括号括起来进行定义,如下: >>> (1,2,3)[1:2] (2,) 如果元组中只有一个元素,元组的表示 ...
- BZOJ 2463: [中山市选2009]谁能赢呢?[智慧]
明和小红经常玩一个博弈游戏.给定一个n×n的棋盘,一个石头被放在棋盘的左上角.他们轮流移动石头.每一回合,选手只能把石头向上,下,左,右四个方向移动一格,并且要求移动到的格子之前不能被访问过.谁不能移 ...
- BZOJ 4566: [Haoi2016]找相同字符 [后缀自动机]
4566: [Haoi2016]找相同字符 Time Limit: 20 Sec Memory Limit: 256 MBSubmit: 275 Solved: 155[Submit][Statu ...
- ES6标准入门 第一章:简介
ECMAScript 6 是JavaScript 语言的下一代标准:发布于2015年,又称为ECMAScript 2015. ECMAScript 与 JavaScript 的关系:前者是后者的规范, ...
- 关于webconsole报../website/console.go:35: undefined: ssh.InsecureIgnoreHostkey 错误解决方案
1.首先,进入webconsole目录删除/opt/webconsole/src/golang.org/x/目录下 crypto文件夹 2.然后,在/opt/webconsole/src/golang ...
- zip-gzip-bzip2_压缩文件
问:为什么要压缩文件? 答:方便传输,因为压缩的文件容量会比较小 存储所使用的空间也会比较小 ---> 备份 Windows里的压缩软件:WinRAR.Zip.好压.2345 ...
- 【转】磁盘I/O那些事
背景 计算机硬件性能在过去十年间的发展普遍遵循摩尔定律,通用计算机的CPU主频早已超过3GHz,内存也进入了普及DDR4的时代.然而传统硬盘虽然在存储容量上增长迅速,但是在读写性能上并无明显提升,同时 ...
- 基于Jquery+Ajax+Json+存储过程 高效分页
在做后台开发中,都会有大量的列表展示,下面给大家给大家分享一套基于Jquery+Ajax+Json+存储过程高效分页列表,只需要传递几个参数即可.当然代码也有改进的地方,如果大家有更好的方法,愿留下宝 ...
- mybatis3:Invalid bound statement (not found)
最近在玩ssm框架搭建,突然发现最后的时候mybaits和SpringMvc进行整合的时候出现错误 Invalid bound statement (not found) 这个错误有可能出现在以下几个 ...
- 3.3 for 循环
Python 编程中 for循环用来遍历序列类型的对象,逐一取出序列中的元素值,每取出一个元素值就执行一次循环体,直到元素取完,循环结束.循环体中的代码块可以和序列中的元素值一点关系都没有,因为for ...