EF访问Sqlite的多线程问题

环境: 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