Timestamap 时间戳

这个东西主要是用来做版本 并发冲突检查的。 在一个实体类当中我们只可以添加一次。

在高并发的时候。 比如说我们的产品有一个库存的数据。这个数据我们希望更新的时候是准确的。 就可以使用这个Attribute了。


 public class Product
    {
        public int Id { get; set; }

        public string Name { get; set; }

        public int StockQuantity { get; set; }

        public DateTime UpdatedOnUtc { get; set; }

        [Timestamp]
        public byte[] Version { get; set; }
    }

数据库

测试一下更新

                var p = dbContext.Products.FirstOrDefault();
                p.StockQuantity += 1;
                dbContext.SaveChanges();

我们可以看到它生成的Sql里面加了一个where.

 SET NOCOUNT ON;
      UPDATE [Products] SET [StockQuantity] = @p0
      WHERE [Id] = @p1 AND [Version] = @p2;

添加数据的时候我们并不需要对Version进行赋值。它会自动生成。更改的时候他也会自动更改。 这个值刚开始是 00 00 00 00 00 00 07 D1 每更改一次就自动加1了。

并发冲突 示例

假设我们默认的 prodcutQuantity是0. 运行这个之后的数值会是 1000吗。 答案是不会。

  public void Run()
        {
            var tasks = new List<Task>();
            for (int i = 0; i < 10; i++)
            {
                var task = Task.Run(() =>
                {
                    for (int j = 0; j < 100; j++)
                    {
                        using (var dbContext = this.dbContextFactory.CreateDbContext())
                        {
                            var p = dbContext.Products.FirstOrDefault();
                            p.StockQuantity += 1;
                            dbContext.SaveChanges();
                        }
                    }
                });
                tasks.Add(task);
            }
            Task.WaitAll(tasks.ToArray());
        }

我们会得到一个并发冲突的导常。

Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException:“Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded.

有冲突时重试

如何处理这个异常保证我们的数据可以正常的更新呢。 比较简单的机制就是加入重试的机制。

 public void Run()
        {
            var tasks = new List<Task>();
            for (int i = 0; i < 10; i++)
            {
                var task = Task.Run(() =>
                {
                    for (int j = 0; j < 100; j++)
                    {
                        Retry();
                    }
                });
                tasks.Add(task);
            }
            Task.WaitAll(tasks.ToArray());
        }

        private void Retry()
        {
            //用For循环来保证。
            for (int m = 0; m < 1000; m++)
            {
                try
                {
                    using (var dbContext = this.dbContextFactory.CreateDbContext())
                    {
                        var p = dbContext.Products.FirstOrDefault();
                        p.StockQuantity += 1;
                        dbContext.SaveChanges();
                    }
                    break;
                }
                catch (DbUpdateConcurrencyException ex)
                {
                    Thread.Sleep(10);
                }
            }
        }

如果冲突确实很多的情况下。这种重试的性能是不太好的。请考虑用锁Lock 上面的代码是类似于CAS操作——Compare & Set

fluent API的配置如下

            modelBuilder.Entity<Product>().Property(x=>x.Version).IsRowVersion();

上面的完整代码可以在分支concurrency/timestamp看到

最近更新的
...