C# 什么';对数据库进行线程安全写缓存的最佳模式是什么?

C# 什么';对数据库进行线程安全写缓存的最佳模式是什么?,c#,multithreading,caching,design-patterns,C#,Multithreading,Caching,Design Patterns,我有一个方法,可以被多个线程调用,将数据写入数据库。为了减少数据库流量,我缓存数据并将其批量写入 现在我想知道,有没有更好的(例如无锁模式)可以使用 下面是一个例子,我现在是如何做到这一点的 public class WriteToDatabase : IWriter, IDisposable { public WriteToDatabase(PLCProtocolServiceConfig currentConfig) {

我有一个方法,可以被多个线程调用,将数据写入数据库。为了减少数据库流量,我缓存数据并将其批量写入

现在我想知道,有没有更好的(例如无锁模式)可以使用

下面是一个例子,我现在是如何做到这一点的

    public class WriteToDatabase : IWriter, IDisposable
    {
        public WriteToDatabase(PLCProtocolServiceConfig currentConfig)
        {
            writeTimer = new System.Threading.Timer(Writer);
            writeTimer.Change((int)currentConfig.WriteToDatabaseTimer.TotalMilliseconds, Timeout.Infinite);
            this.currentConfig = currentConfig;
        }

        private System.Threading.Timer writeTimer;
        private List<PlcProtocolDTO> writeChache = new List<PlcProtocolDTO>();
        private readonly PLCProtocolServiceConfig currentConfig;
        private bool disposed;

        public void Write(PlcProtocolDTO row)
        {
            lock (this)
            {
                writeChache.Add(row);
            }
        }

        private void Writer(object state)
        {
            List<PlcProtocolDTO> oldCachce = null;
            lock (this)
            {
                if (writeChache.Count > 0)
                {
                    oldCachce = writeChache;
                    writeChache = new List<PlcProtocolDTO>();
                }
            }

            if (oldCachce != null)
            {
                    using (var s = VisuDL.CreateSession())
                    {
                        s.Insert(oldCachce);
                    }
            }

            if (!this.disposed)
                writeTimer.Change((int)currentConfig.WriteToDatabaseTimer.TotalMilliseconds, Timeout.Infinite);
        }

        public void Dispose()
        {
            this.disposed = true;
            writeTimer.Dispose();
            Writer(null);
        }
    }
公共类WriteToDatabase:IWriter,IDisposable { 公共WriteToDatabase(PLCProtocolServiceConfig currentConfig) { writeTimer=新系统.Threading.Timer(Writer); writeTimer.Change((int)currentConfig.WriteToDatabaseTimer.TotalMillistics,Timeout.Infinite); this.currentConfig=currentConfig; } 专用System.Threading.Timer writeTimer; 私有列表writeCache=新列表(); 私有只读PLCProtocolServiceConfig currentConfig; 私人住宅; 公共无效写入(PlcProtocolDTO行) { 锁(这个) { writeCache.Add(行); } } 私有无效写入程序(对象状态) { 列表oldcache=null; 锁(这个) { 如果(writeCache.Count>0) { oldcache=写缓存; WriteCache=新列表(); } } 如果(oldcache!=null) { 使用(var s=VisuDL.CreateSession()) { s、 插入(oldcache); } } 如果(!this.disposed) writeTimer.Change((int)currentConfig.WriteToDatabaseTimer.TotalMillistics,Timeout.Infinite); } 公共空间处置() { 这是真的; writeTimer.Dispose(); 编写器(空); } }
不必使用可变的
列表
并使用锁对其进行保护,您可以使用,并且不再担心列表在错误的时间被错误的线程变异的可能性。使用它,传递数据的快照既便宜又容易,因为在创建数据副本时不需要阻止写入程序(也可能是读卡器)。不可变集合本身就是快照

虽然你不必担心收藏的内容,但你仍然需要担心它的参考。这是因为更新不可变集合意味着用新集合替换对旧集合的引用。您不想让多个线程以无法控制的方式交换引用,因此仍然需要某种同步。您仍然可以使用
lock
s,但是通过使用互锁操作可以很容易地避免完全锁定。下面的示例使用方便的方法,允许在一行中执行原子更新和交换:

private ImmutableList<PlcProtocolDTO> writeCache
    = ImmutableList<PlcProtocolDTO>.Empty;

public void Write(PlcProtocolDTO row)
{
    ImmutableInterlocked.Update(ref writeCache, x => x.Add(row));
}

private void Writer(object state)
{
    IList<PlcProtocolDTO> oldCache = Interlocked.Exchange(
        ref writeCache, ImmutableList<PlcProtocolDTO>.Empty);

    using (var s = VisuDL.CreateSession())
        s.Insert(oldCache);
}

private void Dump()
{
    foreach (var row in Volatile.Read(ref writeCache))
        Console.WriteLine(row);
}
私有不可变列表写入缓存
=不可变列表。为空;
公共无效写入(PlcProtocolDTO行)
{
ImmutableInterlocated.Update(ref writeCache,x=>x.Add(行));
}
私有无效写入程序(对象状态)
{
IList oldCache=Interlocked.Exchange(
ref writeCache,ImmutableList.Empty);
使用(var s=VisuDL.CreateSession())
s、 插入(oldCache);
}
私有无效转储()
{
foreach(Volatile.Read(ref writeCache)中的变量行)
控制台写入线(世界其他地区);
}
以下是
ImmutableInterlocated.Update
方法的说明:

通过指定的转换函数,使用乐观锁定事务语义就地变异值。为了赢得乐观锁定竞赛,需要多次重试转换


此方法可用于更新任何类型的引用类型变量。它的使用可能会随着的出现而增加,默认情况下是不可变的,并且打算这样使用。

基于计时器的代码存在一些问题

  • 即使在新版本的代码中,重新启动或关闭时仍有可能丢失写操作。
    Dispose
    方法没有等待完成当前可能正在进行的最后一次计时器回调。 由于计时器回调在线程池线程(后台线程)上运行,因此当主线程退出时,它们将被中止
  • 批处理的大小没有限制,当您达到底层存储api的限制时,这将被打破 (例如,sql数据库对查询长度和使用的参数数量有限制)
  • 因为您正在执行i/o,所以实现可能应该是异步的
  • 这将在负载下表现不佳。 特别是当负载持续增加时,批处理会变得更大,因此执行速度会变慢, 一个较慢的批处理执行反过来将给下一个额外的时间来积累项目,使他们更慢,等等。。。 最终,要么编写批处理失败(如果达到sql限制,要么查询超时),要么应用程序内存不足。 要处理高负载,您实际上只有两种选择,即应用背压(即减慢生产者)或删除写入
  • 如果数据库能够处理,您可能希望允许有限数量的并发写入程序
  • disposed
    字段中存在竞争条件,这可能导致
    writeTimer.Change中出现
    ObjectDisposedException
我认为解决上述问题的更好模式是消费者-生产者模式,您可以在.net中实现它 使用ConcurrentQueue或新的System.Threading.Channels api

还要记住,如果您的应用程序因任何原因崩溃,您将丢失仍在缓冲中的记录

这是一个使用通道的示例实现:

公共接口IWriter { ValueTask WriteAsync(IEnumerable items); } 公开密封记录选项