分布式多线程的Lock示例
场景实例:
现在比较流行的分布式,多线程的项目中,往往会遇到这么一个问题,就是当多个application或者多user并发的访问或者更改数据库同一DB数据时,可能会导致数据的不一致性,那怎么解决呢???
解决方案:
1、第一种方式借用数据库事务(transaction):访问并操作数据项的数据库操作序列,这些操作要不全部执行,要不全部不执行,是一个不可分割的单元。
为什么说数据库事务可以解决这个问题呢?
这就不得不说数据库事务的几个特性了,简称(ACID)
1)、原子性(Atomacity):事务中的全部操作在数据库中是不可分割的,要不全部执行,要不全部不执行
2)、一致性(consistency):几个并行执行的事务,其执行结果和按某一顺序执行的结果必须是一致的。
3)、隔离性(isolation):一个事务的执行不受其他事务的干扰。
4)、永久性(durability):已提交的事务,保证对数据库中数据的修改是不丢失的,即使数据库出现故障。
事务的ACID特性是由关系数据库(DBMS)来实现的,DBMS采用日志来保证数据库的原子性,一致性和永久性,日志记录了事务对数据库所做的更新,如果某个事务在执行过程中发生错误,就可以根据日志撤销事务对数据库所做的更新,使得数据库回滚到事务开始前的状态。
对于事务的隔离性,DBMS是采用锁机制来实现的。当多个事务同时更新数据中相同的数据时,只允许持有锁的事务能更新该数据,其他事务必须等待,知道前一个事务释放了锁,其他事务才有机会更新该数据。
数据库事务的有点:
把逻辑相关的操作分成一个组;
在数据永久改变前可以预览数据变化;
能够保证数据的读写一致性;
2、第二种方式是自己实现锁机制
核心思想:
1)、DB中添加一张表,包括2个字段(LockId, ExpirationTime)
2)、当多个Request需要更新DB中同一数据时,通过LockId Lock住所需要更新这条数据,直到当前request结束后者过期,才开始下一个Request操作
核心代码:
public class CommonLockHelper { private TimeSpan _onceWaitTime; private bool _isWait = true; private static readonly TimeSpan _defaultWaitTime = TimeSpan.FromMinutes(10); private static readonly TimeSpan _defaultLockTimeout = TimeSpan.FromMinutes(30); public CommonLockHelper() { _onceWaitTime = TimeSpan.FromSeconds(5); } public CommonLockHelper(TimeSpan onceWaitTime) { _onceWaitTime = onceWaitTime; } public CommonLockHelper(bool isWait) { if (isWait) { _onceWaitTime = TimeSpan.FromSeconds(5); } else { _onceWaitTime = TimeSpan.FromMilliseconds(1); } _isWait = isWait; } #region for string public LockObject GetLockObject(IDBRepository dBRepository, string actionId) { return GetLockObject(dBRepository, actionId, _defaultWaitTime, _defaultLockTimeout); } public LockObject GetLockObject(IDBRepository dBRepository, string actionId, TimeSpan waitTimeOut) { return GetLockObject(dBRepository, actionId, waitTimeOut, _defaultLockTimeout); } public LockObject GetLockObject(IDBRepository dBRepository, string actionId, TimeSpan waitTimeOut, TimeSpan lockTimeOut) { return GetLockObject(dBRepository, Convert2Guid(actionId), waitTimeOut, lockTimeOut); } #endregion #region for guid public LockObject GetLockObject(IDBRepository dBRepository, Guid actionId) { return GetLockObject(dBRepository, actionId, _defaultWaitTime, _defaultLockTimeout); } public LockObject GetLockObject(IDBRepository dBRepository, Guid actionId, TimeSpan waitTimeOut) { return GetLockObject(dBRepository, actionId, waitTimeOut, _defaultLockTimeout); } public LockObject GetLockObject(IDBRepository dBRepository, Guid actionId, TimeSpan waitTimeOut, TimeSpan lockTimeOut) { DateTime now = DateTime.UtcNow; while (DateTime.UtcNow - now < waitTimeOut) { if (TryAddLockObjectToDatabase(dBRepository, actionId, lockTimeOut)) { return new LockObject(() => { Delete(dBRepository, actionId); }); } if (!_isWait) { return new LockObject(); } } throw new TimeoutException($"wait timeout, actionId:{actionId}, startTimeUTC:{now}, waitTimeOut:{waitTimeOut}"); } #endregion private Guid Convert2Guid(string myStr) { using (var md5 = MD5.Create()) { byte[] hash = md5.ComputeHash(Encoding.UTF8.GetBytes(myStr)); return new Guid(hash); } } private bool TryAddLockObjectToDatabase(IDBRepository dBRepository, Guid actionId, TimeSpan lockTimeOut) { try { var expirationTime = DateTime.UtcNow + lockTimeOut; var lockEntity = FindOne(dBRepository, actionId); if (lockEntity != null) { if (lockEntity.ExpirationTime < DateTime.UtcNow) { //the current lock is expired, update expirationTime and reuse it Update(dBRepository, actionId, expirationTime); } else { //current lock is not expired, wait and return false Thread.Sleep(_onceWaitTime); return false; } } else { Add(dBRepository, actionId, expirationTime); } } catch (Exception e) { Thread.Sleep(_onceWaitTime); return false; } return true; } private DBLock FindOne(IDBRepository dBRepository, Guid actionId) { var sql = @"SELECT * FROM [Common_Lock] WHERE [Id] = @Id"; var result = dBRepository.SqlQuery<DBLock>(sql, new SqlParameter("@Id", actionId)); return result.FirstOrDefault(); } private void Add(IDBRepository dBRepository, Guid actionId, DateTime expirationTime) { var sql = @"INSERT INTO [dbo].[Common_Lock]([Id],[ExpirationTime])VALUES(@Id, @ExpirationTime)"; dBRepository.ExecuteSqlCommand(sql, new SqlParameter("@Id", actionId), new SqlParameter("@ExpirationTime", expirationTime)); } private void Update(IDBRepository dBRepository, Guid actionId, DateTime expirationTime) { var sql = @"UPDATE [Common_Lock] set ExpirationTime = @ExpirationTime WHERE [Id] = @Id"; dBRepository.ExecuteSqlCommand(sql, new SqlParameter("@Id", actionId), new SqlParameter("@ExpirationTime", expirationTime)); } private void Delete(IDBRepository dBRepository, Guid actionId) { var sql = @"DELETE FROM [Common_Lock] WHERE [Id] = @Id"; dBRepository.ExecuteSqlCommand(sql, new SqlParameter("@Id", actionId)); } } public class LockObject : IDisposable { private static readonly AveLogger logger = AveLogger.GetInstance(MethodBase.GetCurrentMethod().DeclaringType); private Action _disposeAction; public bool IsLockEnabled { get; private set; } public LockObject(Action disposeAction) { _disposeAction = disposeAction; IsLockEnabled = true; } public LockObject() { IsLockEnabled = false; } public void Dispose() { try { _disposeAction?.Invoke(); } catch (Exception e) { } } }