EF Core 乐观、悲观并发控制

ops/2025/1/23 13:02:48/

目录

并发控制的概念

悲观并发控制

实现

问题

乐观并发控制

实现

RowVersion

实体类及配置

概念

总结


并发控制的概念

  1. 并发控制:避免多个用户同时操作资源造成的并发冲突问题。举例:统计点击量。
  2. 最好的解决方案:非数据库解决方案。
  3. 数据库层面的两种策略:悲观、乐观。

悲观并发控制

悲观并发控制一般采用行锁、表锁等排他锁对资源进行锁定,确保同时只有一个使用者操作被锁定的资源。

EF Core没有封装悲观并发控制的使用,需要开发人员编写原生SQL语句来使用悲观并发控制。不同数据库的语法不一样。

实现

SQL Server:select * from T_Houses with(updlock) where id=1

如果有其他的查询操作也使用行级锁来查询Id=1的这条数据的话,那些查询就会被挂起,一直到针对这条数据的更新操作完成从而释放这个行锁,代码才会继续执行。

锁是和事务相关的,因此通过BeginTransactionAsync()创建一个事务,并且在所有操作完成后调用CommitAsync()提交事务。

public class Program
{class House{public long Id { get; set; }public string Name { get; set; }public string? Owner { get; set; }}static async Task Main(string[] args){//方法1//var tom = ChooseHouseAsync("Tom");//var lucy = ChooseHouseAsync("Lucy");//await Task.WhenAll(tom, lucy);//方法2ChooseHouseAsync("Tom");await ChooseHouseAsync("Lucy");}public static async Task ChooseHouseAsync(string name){using (MyDbContext ctx = new MyDbContext())using (var tx = await ctx.Database.BeginTransactionAsync()){Console.WriteLine($"{name}:准备开启查询锁{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");var h = await ctx.Houses.FromSqlInterpolated($"select * from T_Houses with (updlock) where id=1").FirstOrDefaultAsync();Console.WriteLine($"{name}:查询锁开启成功{DateTime.Now}.ToString(\"yyyy-MM-dd HH:mm:ss.fff\")");if (!string.IsNullOrEmpty(h.Owner)){if (h.Owner == name){Console.WriteLine($"{name}:房子已经被你抢到了");}else{Console.WriteLine($"{name}:房子已经被{h.Owner}占了");}return;}await Task.Delay(5000);h.Owner = name;Console.WriteLine($"{name}:恭喜你抢到了");await ctx.SaveChangesAsync();//执行完释放锁Console.WriteLine($"{name}:释放锁{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");await tx.CommitAsync();}}
}

问题

  1. 悲观并发控制的使用比较简单;
  2. 锁是独占、排他的,如果系统并发量很大的话,会严重影响性能,如果使用不当的话,甚至会导致死锁。
  3. 不同数据库的语法不一样。

乐观并发控制

Update T_Houses set Owner=新值 where Id=1 and Owner=旧值

当Update的时候,如果数据库中的Owner值已经被其他操作者更新为其他值了,那么where语句的值就会为false,因此这个Update语句影响的行数就是0,EF Core就知道“发生并发冲突”了,因此SaveChanges()方法就会抛出DbUpdateConcurrencyException异常。

把被并发修改的属性使用IsConcurrencyToken()设置为并发令牌。

builder.Property(h => h.Owner).IsConcurrencyToken();

实现

public static async Task ChooseHouseAsync(string name)
{Console.WriteLine($"{name}开始执行");using (MyDbContext ctx = new MyDbContext()){Console.WriteLine($"{name}:准备查询{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");var h = await ctx.Houses.FromSqlInterpolated($"select * from T_Houses where id=1 and (Owner is null or Owner='')").FirstOrDefaultAsync();Console.WriteLine($"{name}:查询完成{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");if (!string.IsNullOrEmpty(h.Owner)){if (h.Owner == name){Console.WriteLine($"{name}:房子已经被你抢到了");}else{Console.WriteLine($"{name}:房子已经被{h.Owner}占了");}return;}await Task.Delay(5000);h.Owner = name;try{await ctx.SaveChangesAsync();}catch (DbUpdateConcurrencyException ex){var entry=ex.Entries.First();var dbValues=await entry.GetDatabaseValuesAsync();string newOwner=dbValues.GetValue<string>(nameof(House.Owner));Console.WriteLine($"{name}:并发访问冲突,被{newOwner}抢走了");}}
}

RowVersion

  1. SQLServer数据库可以用一个byte[]类型的属性做并发令牌属性,然后使用IsRowVersion()把这个属性设置为RowVersion类型,这样这个属性对应的数据库列就会被设置为ROWVERSION类型。对于ROWVERSION类型的列,在每次插入或更新行时,数据库会自动为这一行的ROWVERSION类型的列其生成新值。
  2. 在SQLServer中,timestamp和rowversion是同一种类型的不同别名而已。

实体类及配置

class House
{public long Id { get; set; }public string Name { get; set; }public string? Owner { get; set; }public byte[] RowVersion { get; set; }
}builder.Property(o=>o.RowVersion).IsRowVersion();

概念

  1. 在MySQL等数据库中虽然也有类似的timestamp类型,但是由于timestamp类型的精度不够,并不适合在高并发的系统。
  2. 非SQLServer中,可以将并发令牌列的值更新为Guid的值。
  3. 修改其他属性值的同时,使用h1.RowVersion = Guid.NewGuid()手动更新并发令牌属性的值。

总结

  1. 乐观并发控制能够避免悲观锁带来的性能、死锁等问题,因此推荐使用乐观并发控制而不是悲观锁。
  2. 如果有一个确定的字段要被进行并发控制,那么使用IsConcurrencyToken()把这个字段设置为并发令牌即可; 
  3. 如果无法确定一个唯一的并发令牌列,那么就可以引入一个额外的属性设置为并发令牌,并且在每次更新数据的时候,手动更新这一列的值。如果用的是SQLServer数据库,那么也可以采用RowVersion列,这样就不用开发者手动来在每次更新数据的时候,手动更新并发令牌的值了。

http://www.ppmy.cn/ops/152473.html

相关文章

IJCAI-2024 | 具身导航的花样Prompts!VLN-MP:利用多模态Prompts增强视觉语言导航能力

作者&#xff1a; Haodong Hong1,2 , Sen Wang1∗ , Zi Huang1 , Qi Wu3 and Jiajun Liu2,1 单位&#xff1a;昆士兰大学&#xff0c;澳大利亚科学与工业研究组织&#xff0c;阿德莱德大学 论文标题&#xff1a;Why Only Text: Empowering Vision-and-Language Navigation wi…

maven常见知识点

1、maven是什么&#xff1f; maven是Java的包管理工具&#xff0c;因为java包太多了&#xff0c;使用工具统一管理。 2、引入同一个包时使用哪个&#xff1f; 会遵循 路径最短优先 和 声明顺序优先 两大原则。解决这个问题的过程也被称为 Maven 依赖调解。 3、什么是 POM&…

Netty搭建websocket服务器,postman可以连接,浏览器无法连接

简介&#xff1a;Netty搭建websocket服务器&#xff0c;postman可以连接&#xff0c;浏览器无法连接&#xff0c;很奇怪&#xff0c;不知道为什么。最后更换端口解决问题&#xff0c;原来端口时6666&#xff0c;把6666改成其他端口就可以了。 过程&#xff1a; 前端代码 后端…

MySQL——主从同步

提醒&#xff1a;进行配置时&#xff0c;需要确保一主两从的操作系统、MySQL版本一致&#xff0c;否则将出现问题 环境介绍 服务器IP主服务器172.25.254.10从服务器-1172.25.254.11从服务器-2172.25.254.12 配置 # 快速配置&#xff0c;选择多重执行&#xff0c;确保版本一…

docker load报错(unexpected EOF)

今天解决了一个困扰我2天的问题&#xff0c;那就是docker load 失败&#xff0c;背景是这样的&#xff0c;同事离职交接给我一个基础docker镜像文件&#xff0c;大约600M&#xff0c;然后我把这个文件拖到Centos7虚拟机中&#xff0c;然而docker load的时候报错了&#xff0c;错…

java开发之文件上传

前端&#xff1a;必须设置表单的内容格式为multipart/form-data&#xff0c;必须有file表单项&#xff0c;method必须为POST 服务器端&#xff1a;用MultipartFile格式接受文件 文件存储 本地存储&#xff1a;存储到服务器本地磁盘目录。调用MultipartFile的transferTo()方…

于灵动的变量变幻间:函数与计算逻辑的浪漫交织(下)

大家好啊&#xff0c;我是小象٩(๑ω๑)۶ 我的博客&#xff1a;Xiao Xiangζั͡ޓއއ 很高兴见到大家&#xff0c;希望能够和大家一起交流学习&#xff0c;共同进步。 这一节我们主要来学习单个函数的声明与定义&#xff0c;static和extern… 这里写目录标题 一、单个函数…

python实现答题游戏

有这样一个需求&#xff1a;使用python实现一个游戏&#xff0c;一共有10个问题&#xff0c;依次回答每个问题&#xff0c;每个用户可以输入问题的答案&#xff0c;但是互相不能看到&#xff0c;有一个管理员可以看到所有人的答案&#xff0c;并且当所有人都填写完成后可以公布…