Skip to main content

并发冲突

我们在插入和更新数据时,经常会因为并发等原因发生冲突。Obase将并发冲突分为三类,重复创建,版本冲突,更新幻影。 重复创建指的是两个向持久层尝试写入主键已存在的对象,这常常发生在一些竞争性插入的并发场景;版本冲突指那些A进程查询得到的对象,在保存时已被B进程修改而导致的;最后,更新幻影指A进程查询得到的对象,在保存时已被B进程删除而导致的。 那么Obase引入了五种不同的策略来应对这三种冲突,他们分别是忽略(Ignore),引发异常(ThrowException),强制覆盖(Overwrite),版本合并(Combine)和重建对象(Reconstruct)。

  • 忽略策略:这一策略会忽略产生的任何异常,对于三种并发冲突均会忽略他的发生。
  • 引发异常:这一策略会将异常捕获,并分别抛出Obase定义的重复创建异常RepeatCreationException,版本冲突异常VersionConflictException和更新幻影异常UpdatingPhantomException。在没有针对实体型进行并行异常处理策略指定的情况下,默认使用此种策略。
  • 强制覆盖:这一策略会使用当前的对象直接覆盖原对象,对于三种并发冲突,更新幻影不适用于此种策略。
  • 版本合并:这一策略会使用以下几种合并模式将现对象与原对象的指定属性合并,覆盖(Overwrite)即将现对象的指定属性值覆盖原对象的,忽略(Ignore)即抛弃现对象的值而承认原对象的值,累加(Accumulate)即将当前版本中属性值的增量累加到对方版本。对于三种并发冲突,更新幻影不适用于此种策略。
  • 重建对象:此种策略将新建的新对象插入指定的对象集合内,对于三种并发冲突,重复创建和版本冲突时不适用此种策略。

那么接下来我就用一个类来演示并发策略:

/// <summary>
/// 带有版本键的键值对
/// </summary>
public class KeyValueWithVersion
{
/// <summary>
/// 唯一标识
/// </summary>
private int _id;

/// <summary>
/// 键
/// </summary>
private string _key;

/// <summary>
/// 值
/// </summary>
private int _value;

/// <summary>
/// 版本键
/// </summary>
private int _versionKey;

/// <summary>
/// 键
/// </summary>
public string Key
{
get => _key;
set => _key = value;
}

/// <summary>
/// 值
/// </summary>
public int Value
{
get => _value;
set => _value = value;
}

/// <summary>
/// 版本键
/// </summary>
public int VersionKey
{
get => _versionKey;
set => _versionKey = value;
}

/// <summary>
/// 唯一标识
/// </summary>
public int Id
{
get => _id;
set => _id = value;
}

/// <summary>
/// 返回字符串表示形式
/// </summary>
/// <returns></returns>
public override string ToString()
{
return $"KeyValueWithVersion:{Id-{_id},Key-\"{_key}\",Value-{_value},VersionKey-{_versionKey}";
}
}

这里他的ID是主键。 首先我们先将此类配置为实体型:

modelBuilder.Entity<KeyValueWithVersion>().HasKeyAttribute(p => p.Id).HasKeyIsSelfIncreased(false)

这里我们引入一个版本键的概念,当你遇到A进程查询得到的对象,在保存时已被B进程修改而导致冲突时,可以引入版本键,版本键应当是一个简单类型,每次修改对象时,将版本键一并修改并加以保存,即可区分出对象是否被其他进程修改。 那么针对这个实体型,我们为他配置一个版本键和如下的并发处理策略:

  • 忽略 忽略发生的异常。这样配置即可:
modelBuilder.Entity<KeyValueWithVersion>()
.HasKeyAttribute(p => p.Id).HasKeyIsSelfIncreased(false)
.HasConcurrentConflictHandlingStrategy(eConcurrentConflictHandlingStrategy.Ignore)
.HasKeyAttribute(p=>p.VersionKey);

使用如下的代码作为示例:

//构造对象
var ingoreSimpleKeyValue = new IngoreKeyValue
{
Id = 1,
Key = "Key",
Value = 1,
VersionKey = 1
};

//附加
var context = new DbContext();
context.IngoreKeyValues.Attach(ingoreSimpleKeyValue);
context.SaveChanges();

//重复插入 异常忽略
ingoreSimpleKeyValue = new IngoreKeyValue
{
Id = 1,
Key = "Key",
Value = 2,
VersionKey = 1
};
//异常被忽略
context.IngoreKeyValues.Attach(ingoreSimpleKeyValue);
context.SaveChanges();

这里模拟了一个插入重复主键的情况,在忽略策略下,异常会在Obase内被捕获并处理,所以持久层内仍是第一次插入的对象。

//修改
queryIngoreSimpleKeyValue.Value = 2;
//模拟一个版本键被修改
context.IngoreKeyValues.SetAttributes(new[] {new KeyValuePair<string, object>("VersionKey", 2)},
p => p.Id == 1);
//异常被忽略
context.SaveChanges();

接下来模拟的是对象被其他进程修改的情况:

//修改
queryIngoreSimpleKeyValue.Value = 9;
//模拟一个版本键被修改
context.IngoreKeyValues.SetAttributes(new[] {new KeyValuePair<string, object>("VersionKey", 2)},
p => p.Id == 1);
//异常被忽略
context.SaveChanges();

此时异常也会被忽略掉,同之前一样,持久层内仍是第一次插入的对象(版本键被修改后)。 注意,这两段测试代码会在之后的其他策略中继续使用。

  • 引发异常 将之前的配置修改为以下配置:
modelBuilder.Entity<KeyValueWithVersion>()
.HasKeyAttribute(p => p.Id)
.HasKeyIsSelfIncreased(false).HasKeyAttribute(p=>p.VersionKey);

因为抛出异常是默认的设置,所以不需要指定。那么对于之前的测试代码,使用引发异常配置时会在第一段测试代码中抛出RepeatCreationException异常,在第二段测试代码中抛出VersionConflictException异常。

  • 强制覆盖 将之前的配置修改为以下配置:
modelBuilder.Entity<KeyValueWithVersion>().HasKeyAttribute(p => p.Id)
.HasKeyIsSelfIncreased(false)
.HasConcurrentConflictHandlingStrategy(eConcurrentConflictHandlingStrategy.Overwrite)
.HasKeyAttribute(p=>p.VersionKey);

如此配置后,针对之前的测试代码,使用强制覆盖配置时第一段测试代码中对象会被覆盖为之后的Value为2的对象,在第二段测试代码中对象会被覆盖为之后的Value为9的对象。

  • 版本合并 将之前的配置修改为以下配置:
var keyValueCfg = modelBuilder.Entity<KeyValueWithVersion>()
.HasKeyAttribute(p => p.Id).HasKeyIsSelfIncreased(false)
.HasConcurrentConflictHandlingStrategy(eConcurrentConflictHandlingStrategy.Combine)
.HasKeyAttribute(p=>p.VersionKey);

对于版本合并策略,我们还要额外指定合并哪些属性,以及使用什么合并策略,所以这里用一个变量存储了实体型的配置,并加入如下的配置:

keyValueCfg.Attribute(p => p.Value).HasCombinationHandler(eAttributeCombinationHandlingStrategy.Overwrite);

这里即是使用覆盖合并策略,那么对于之前的测试代码,第一段的结果会被覆盖成Value为2的新对象;第二段的结果也和第一段一样,也会被覆盖成新对象。 然后我们将合并的模式进行修改,改为以下配置:

keyValueCfg.Attribute(p => p.Value).HasCombinationHandler(eAttributeCombinationHandlingStrategy.Ignore);

那么对于之前的测试代码,第一段的结果仍是原对象,因为没有属性被合并,他们被忽略掉了;第二段的结果也和第一段一样,仍为原对象。 接着是最后一种,累加模式:

keyValueCfg.Attribute(p => p.Value).HasCombinationHandler(eAttributeCombinationHandlingStrategy.Accumulate);

对于之前的测试代码,第一段的结果会被累加成Value为2的新对象;第二段的结果也和第一段一样,Value属性也会被累加成新对象。这里的结果和之前的覆盖是类似的,但内部实现是不同的。

  • 重建对象 将之前的配置修改为以下配置:
modelBuilder.Entity<KeyValueWithVersion>().HasKeyAttribute(p => p.Id).HasKeyIsSelfIncreased(false)
.HasConcurrentConflictHandlingStrategy(eConcurrentConflictHandlingStrategy.Reconstruct)
.HasKeyAttribute(p=>p.VersionKey);

重建对象策略只对更新幻影并发冲突有效,测试代码如下:

//构造对象
var reconstructeKeyValue = new ReconstructKeyValue
{
Id = 1,
Key = "Key",
Value = 1,
VersionKey = 1
};

//附加
var context = new DbContext();
context.ReconstructKeyValues.Attach(reconstructeKeyValue);
context.SaveChanges();

var queryReconstructKeyValue = context.ReconstructKeyValues.FirstOrDefault(p => p.Id == 1);

Assert.IsNotNull(queryReconstructKeyValue);
Assert.AreEqual("Key", queryReconstructKeyValue.Key);
Assert.AreEqual(1, queryReconstructKeyValue.Value);

Console.WriteLine(queryReconstructKeyValue);

//修改
queryReconstructKeyValue.Value = 4;
//模拟一个主键被修改
context.IngoreKeyValues.SetAttributes(new[] {new KeyValuePair<string, object>("Id", 2)},
p => p.Id == 1);
//会被覆盖
context.SaveChanges();

在主键被修改的情况下,会新增一个ID为1,Value为4的新对象。

最后,如果版本合并的策略中,要合并的属性位于复杂类型上要如何配置?我们来看一个新的示例:

/// <summary>
/// 作为复杂属性的键值对
/// </summary>
public struct ComplexKeyValue
{
/// <summary>
/// 键
/// </summary>
public string Key { get; set; }

/// <summary>
/// 值
/// </summary>
public int Value { get; set; }
}

/// <summary>
/// 带有复杂类型的版本键的键值对
/// </summary>
public class ComplexKeyValueWithVersion
{
/// <summary>
/// 键值对
/// </summary>
private ComplexKeyValue _keyValue;


/// <summary>
/// 唯一标识
/// </summary>
private int _id;

/// <summary>
/// 版本键
/// </summary>
private int _versionKey;

/// <summary>
/// 唯一标识
/// </summary>
public int Id
{
get => _id;
set => _id = value;
}

/// <summary>
/// 版本键
/// </summary>
public int VersionKey
{
get => _versionKey;
set => _versionKey = value;
}

/// <summary>
/// 键值对
/// </summary>
public ComplexKeyValue KeyValue
{
get => _keyValue;
set => _keyValue = value;
}
}

这个示例和之前的KeyValueWithVersion使用同一个MySql表,他将Key和Value包装成了一个复杂类型,如果我们和之前一样仍要使用合并策略并且合并复杂属性的Value字段,那么可以进行如下的配置:

//配置复杂类型
var overWriteComplexConbineConfiguration = modelBuilder.Complex<ComplexKeyValue>();
overWriteComplexConbineConfiguration.Attribute("Value", typeof(int)).HasValueGetter(p => p.Value)
.HasValueSetter(p => p.Value)
.HasCombinationHandler(eAttributeCombinationHandlingStrategy.Overwrite);
//配置具体的实体型
var complexOverWriteCombineConfig = modelBuilder.Entity<ComplexKeyValueWithVersion>();
complexOverWriteCombineConfig.HasKeyAttribute(p => p.Id).HasKeyIsSelfIncreased(false);
complexOverWriteCombineConfig.HasConcurrentConflictHandlingStrategy(eConcurrentConflictHandlingStrategy
.Combine);
complexOverWriteCombineConfig.HasVersionAttribute(p => p.VersionKey);
complexOverWriteCombineConfig.ToTable("KeyValues");

对于复杂类型的属性,我们可以根据实际需求配置具体的合并模式。对于其他类似配置,鼓励读者自行探索和尝试,以实现更灵活的数据处理。