并发的危害
多个用户(线程)在争强同一资源。如果发生 程序和数据的一致性、可靠性、唯一性都可能受到破坏。如果没有资源竞争或者没有重复的增量并发就不叫并发而叫并行了。解决并发的各种方案和思路个人感觉万变不离其中,都是将并行的事件转换为串行执行,下面我根据个人对并发的出现场景和解决方案做一定的分享欢迎拍砖这样大家就可以互相学习。
多线程的并发
多线程一般用于同时处理多个并行的任务,当并行任务出现资源争抢,我们第一个想到的解决方案就是锁 lock,使用了锁之后就会让并行的任务等待并且串行执行。要注意的就是主线程或者控制线程不要有过多的堵塞,这样会给客户带来不好的体验度。在网上看到一篇不错的博客提供了一个方法来处理了这种类型的问题,不过他只使用了一个额外的worker线程处理任务。不论外部使用多少个线程访问他都通过队列和线程通知 巧妙的绕开 代码如下
public class Logger { /// <summary> /// 存放写日志任务的队列 /// </summary> private Queue<Action> _queue; /// <summary> /// 用于写日志的线程 /// </summary> private Thread _loggingThread; /// <summary> /// 用于通知是否有新日志要写的“信号器” /// </summary> private ManualResetEvent _hasNew; /// <summary> /// 单例模式,保持一个Logger对象实例 /// </summary> private static readonly Logger _logger = new Logger(); private static Logger GetInstance() { /* 不安全代码 lock (locker) { if (_logger == null) { _logger = new Logger(); } }*/ return _logger; } private void Process() { while (true) { // 等待接收信号,阻塞线程。 _hasNew.WaitOne(); // 接收到信号后,重置“信号器”,信号关闭。 _hasNew.Reset(); // 由于队列中的任务可能在极速地增加,这里等待是为了一次能处理更多的任务,减少对队列的频繁“进出”操作。 Thread.Sleep(100); // 开始执行队列中的任务。 // 由于执行过程中还可能会有新的任务,所以不能直接对原来的 _queue 进行操作, // 先将_queue中的任务复制一份后将其清空,然后对这份拷贝进行操作。 Queue<Action> queueCopy; lock (_queue) { queueCopy = new Queue<Action>(_queue); _queue.Clear(); } foreach (var action in queueCopy) { action(); } } } /// <summary> /// 构造函数,初始化。 /// </summary> private Logger() { _queue = new Queue<Action>(); _hasNew = new ManualResetEvent(false); _loggingThread = new Thread(Process); _loggingThread.IsBackground = true; _loggingThread.Start(); } private void WriteLog(string content) { lock (_queue) { // todo: 这里存在线程安全问题,可能会发生阻塞。 // 将任务加到队列 _queue.Enqueue(() => File.AppendAllText("log.txt", content+" runtime:"+DateTime.Now.ToString())); } // 打开“信号” _hasNew.Set(); } // 公开一个Write方法供外部调用 public static void Write(string content) { // WriteLog 方法只是向队列中添加任务,执行时间极短,所以使用Task.Run。 Task.Run(() => GetInstance().WriteLog(content)); }
当外部线程过多WriteLog方法中的锁就可能会出现堵塞。目前没有测试出堵塞与机器性能也有一定关系
非分布式系统并发解决方案
insert并发
方案一 设置唯一列非主键,每次添加先查询最后一列数据的 唯一列值 做递增量由业务程序控制,原理通过唯一约束引发异常方案比较稳定可行。
方案二 业务程序级锁控制,使用条件改业务系统不能为分布式部署并且,只有一个insert方法添加数据。
方案三 数据库事务独占锁(排它锁)
个人目前想到较为可靠的方案,希望看到的朋友能帮我补充。
update delete 并发
方案一 依然是数据库上的处理方案,添加一个timestamp类型字段,如果没有该类型的字段可以使用触发器或者自己业务程序实现一个版本号字段原理很简单每次 CRUD操作都递增该字段。当update一行数据将timestamp 作为必要条件带入
Update xx set xxx =xxx where key='1' and timestamp='0x000000000000272C'
方案二 依然是业务程序的锁
lock 语法糖锁 Monitor ReaderWriterLock 读写锁 性能低下 不受欢迎的锁很多问题 以后专门解释 ReaderWriterLockSlim 类支持三种锁定模式:Read,Write,UpgradeableRead
方案三 数据库事务独占锁(排它锁)
独占锁 可以到毫秒级 不过不能设置1毫秒至少为2毫秒 begin tran SELECT getdate() WAITFOR DELAY '00:00:01' SELECT getdate() commit tran begin tran SELECT getdate() WAITFOR DELAY '00:00:00.04' SELECT getdate() commit tran
分布式系统并发解决方案
分布式系统处理并发,最好将并发业务单独剥离,交由一台服务机处理,其他机器调用这一台服务机的接口。这种情况这一台服务机压力也异常大,那么尽量减少并发业务粒度。我们目前一般开发系统还是使用关系型数据库,那么数据库分布式的并不多,最多就是做读写分离,非分布式系统的方案一依然通用。可能产生并发的事务处理最好也在同一库处理,就算你分库产生并发的业务数据也应该是在同一库中。像什么 DRDS这种高端的目前还没有用过更没有解决方案!