环境: winform项目,使用EF6访问Sqlite,CodeFirst模式。所有组件都是直接在VS的Nuget管理器中下载的。
问题:我的一张Users表里有3个用户:张三、李四、王五,每个用户都拥有初始积分1000。我开启两个线程,线程内各自创建自己的dbcontext,然后各自去修改3个用户的积分。第一个线程是对每个用户积分都加100,第二个线程是对“张三”这一个玩家的积分减50。期望得到的结果应该是张三比其它玩家的积分少50,但实际执行时,常常出现,张三的积分是950,其它用户是1100,相差了150。这里应该是第二个线程后执行,且覆盖了第一个线程的写操作。怎么会出现这种情况呢?按说Sqlite作为数据库,不能保证这种基本的原子性吗?
代码:
访问Sqlite的DbContext类
public class SqliteContext : DbContext
{
public DbSet<User> Users { get; set; }
public SqliteContext() : base("SQLiteConnect")
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
var init = new SqliteCreateDatabaseIfNotExists<SqliteContext>(modelBuilder);
Database.SetInitializer(init);
}
}
添加初始数据:
private void Form1_Load(object sender, EventArgs e)
{
//添加初始用户
List<User> userList = new List<User>();
userList.Add(new User() { Name = "张三", Score = 1000 });
userList.Add(new User() { Name = "李四", Score = 1000 });
userList.Add(new User() { Name = "王五", Score = 1000 });
using (var context = new SqliteContext())
{
foreach (var item in userList)
{
var user = context.Users.Where(s => s.Name == item.Name).FirstOrDefault();
if (user != null)
{
continue;
}
else
{
context.Users.Add(item);
}
}
context.SaveChanges();
}
}
开启线程去修改用户积分:
private void button1_Click(object sender, EventArgs e)
{
//执行加分操作
Task.Factory.StartNew(ModifyAllUserScore);
Task.Factory.StartNew(ModifySingleUserScore);
}
//为所有用户加100分
private async Task ModifyAllUserScore()
{
using (var context = new SqliteContext())
{
//先等待1秒
await Task.Delay(1000);
foreach (var user in context.Users)
{
user.Score += 100;
}
await context.SaveChangesAsync();
}
}
//仅为“张三”减少50分
private async Task ModifySingleUserScore()
{
using (var context = new SqliteContext())
{
//先等待1秒
await Task.Delay(1000);
var user = await context.Users.Where(s => s.Name == "张三").FirstOrDefaultAsync();
if (user != null)
{
user.Score -= 50;
}
await context.SaveChangesAsync();
}
}
var user = await context.Users.Where(s => s.Name == "张三").FirstOrDefaultAsync();
这句和更新+100顺序不可控,有可能读取数据到内存中的user对象积分为1000 ,线程1才执行+100操作,内存中张三积分是1000,这样是直接从1000积分操作,而不是更新后的1100。所以出现950的问题。2个context是不同的事务,数据肯定不会同步的。
要么就改成执行sql语句,这样并发出现更新张三的可能性小一些,不需要读取数据库后生成user对象这些耗时操作
context.Database.ExecuteSqlCommand("update users set socre-=50 where name='张三'");
object lokcer = new object();//申明一个全局变量
需要ef操作数据库的时候lock下
lock (lokcer) {
//ef操作的代码
}
linq to sql有并发冲突检查,如果实体对象数据和读取数据库时的不一致会报错。。但是这个应该不是你想要的效果。
你的要求感觉没有哪个框架会实现,因为不会更新前还会再读一次数据库更新实体对象。而且就算再次读的时候不一定就是最新的
感谢你的回答。你的回答指出了问题,但仍无法彻底解决我的问题。
1. 仅仅将第二个线程的操作改为SQL语句是不行的,必须要将两个线程的操作都改为SQL语句。
2. 我采用EF来访问Sqlite就不是想摆脱ADO.NET时代写SQL语句的麻烦,那现在又得倒退回去了
3. 我所贴出来的代码只是一个测试工程,这个问题是在我的实际项目中遇到的。如果要这样修改,则需要检查、修改和测试的地方非常多。
我之前采用EF访问SqlServer,编写后端服务,没遇到过这种问题。所以我想着,是不是通过某些配置或者架构,能够编写出线程安全的访问模式,从而不用去检查和修改业务代码。
您好,我是有问必答小助手,您的问题已经有小伙伴解答了,您看下是否解决,可以追评进行沟通哦~
如果有您比较满意的答案 / 帮您提供解决思路的答案,可以点击【采纳】按钮,给回答的小伙伴一些鼓励哦~~
ps: 问答会员年卡【8折】购 ,限时加赠IT实体书,即可 享受50次 有问必答服务,了解详情>>>https://t.csdnimg.cn/RW5m