小故事
在开始讲这篇文章之前, 我们来说一个小故事, 纯素虚构(真实的存钱逻辑并非如此)
小刘发工资后, 赶忙拿着现金去银行, 准备把钱存起来, 而与此同时, 小刘的老婆刘嫂知道小刘的品性, 知道他发工资的日子, 也知道他喜欢一发工资就去银行存起来, 担心小刘卡里存的钱太多拿去大宝剑, 于是, 也去了银行, 想趁着小刘把钱存进去后就把钱给取出来, 省的夜长梦多
小刘与刘嫂取得是两家不同的银行的 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");
- services.AddDbContext<SchoolContext>(options =>
- {
- options.UseMySql(connection);
- options.UseLoggerFactory(new LoggerFactory().AddConsole());
- });
- 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 != 0)
- {
- 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 = 1000;
- for (int i = 0; i < 10; i++)
- {
- Thread td = new Thread(new ParameterizedThreadStart(PostTest));
- td.Start(i);
- Thread.Sleep(new Random().Next(1,100));// 随机休眠时长
- }
- 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
来源: https://www.cnblogs.com/CoderAyu/p/8530798.html