背景
之前写了一篇文章防止并发修改 之 离线悲观锁代码示例(离线悲观锁),这篇文章回避了一个问题,就是如何处理用户直接关闭浏览器后导致的锁占用问题。本文就介绍一个思路。
思路
思路1
这是之前已经提供过的思路,只是没有贴出来,就是:当会话结束的时候清除所有用户持有的锁,这会导致个别锁在会话期间被长时间占用(可能超过几个小时)。
思路2
引入一个后台线程,每隔指定的分钟就清理一下被长时间占用的锁,如:清理那些占用超过10分钟的锁,这回导致一定的线程成本,因为这个线程需要频繁的运行。
思路3
引入过期策略,是否被锁完全取决于两个条件:是否拥有锁以及是否过期,这个思路下过期的锁会成为一种垃圾,如何清理这种垃圾又是一个问题,我们可以每6个小时清理一次或引入环形字典。
基于过期策略的实现
类图
代码
基于内存的离线悲观锁管理器
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 using Happy.DesignByContract; 8 using Happy.Application.PessimisticLock.Internal; 9 10 namespace Happy.Application.PessimisticLock 11 { 12 /// <summary> 13 /// 基于内存的离线悲观锁管理器。 14 /// </summary> 15 public sealed class MemoryLockManager : ILockManager 16 { 17 private static readonly Dictionary<string, LockItem> _items = new Dictionary<string, LockItem>(); 18 19 /// <inheritdoc /> 20 public bool AcquireLock(string entity, string key, string owner, IExpirationPolicy expirationPolicy) 21 { 22 entity.MustNotNullAndNotWhiteSpace("entity"); 23 key.MustNotNullAndNotWhiteSpace("key"); 24 owner.MustNotNullAndNotWhiteSpace("owner"); 25 expirationPolicy.MustNotNull("expirationPolicy"); 26 27 var item = LockItem.Crete(entity, key, owner, expirationPolicy); 28 29 lock (_items) 30 { 31 if (!IsLocked(item.Identifier)) 32 { 33 LockIt(item); 34 35 return true; 36 } 37 38 return IsLockedBy(item.Identifier, item.Owner); 39 } 40 } 41 42 /// <inheritdoc /> 43 public void ReleaseLock(string entity, string key, string owner) 44 { 45 entity.MustNotNullAndNotWhiteSpace("entity"); 46 key.MustNotNullAndNotWhiteSpace("key"); 47 owner.MustNotNullAndNotWhiteSpace("owner"); 48 49 var identifier = LockItem.CreateIdentifier(entity, key); 50 lock (_items) 51 { 52 if (!IsLockedBy(identifier, owner)) 53 { 54 throw new InvalidOperationException(string.Format(Messages.Error_CanNotReleaseLock, owner)); 55 } 56 57 ReleaseLock(identifier); 58 } 59 } 60 61 /// <inheritdoc /> 62 public void ReleaseLocks(string owner) 63 { 64 lock (_items) 65 { 66 foreach (var keypair in _items) 67 { 68 if (keypair.Value.Owner == owner) 69 { 70 ReleaseLock(keypair.Value.Identifier); 71 } 72 } 73 } 74 } 75 76 /// <inheritdoc /> 77 public void ReleaseExpiredLocks() 78 { 79 lock (_items) 80 { 81 foreach (var keypair in _items) 82 { 83 if (keypair.Value.ExpirationPolicy.IsExpired()) 84 { 85 ReleaseLock(keypair.Value.Identifier); 86 } 87 } 88 } 89 } 90 91 private static bool IsLocked(string identifier) 92 { 93 return 94 _items.ContainsKey(identifier) 95 && 96 !_items[identifier].ExpirationPolicy.IsExpired(); 97 } 98 99 private static bool IsLockedBy(string identifier, string owner) 100 { 101 if (!IsLocked(identifier)) 102 { 103 return false; 104 } 105 106 return _items[identifier].Owner == owner; 107 } 108 109 private static void LockIt(LockItem item) 110 { 111 _items[item.Identifier] = item; 112 } 113 114 private static void ReleaseLock(string identifier) 115 { 116 _items.Remove(identifier); 117 } 118 } 119 }
基于时间的过期策略
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace Happy.Application.PessimisticLock 8 { 9 /// <summary> 10 /// 基于时间的过期策略。 11 /// </summary> 12 [Serializable] 13 public class DateTimeExpirationPolicy : IExpirationPolicy 14 { 15 private readonly DateTime _start = DateTime.Now; 16 private readonly TimeSpan _expiration; 17 18 /// <summary> 19 /// 构造方法。 20 /// </summary> 21 /// <param name="expiration">过期时间间隔</param> 22 public DateTimeExpirationPolicy(TimeSpan expiration) 23 { 24 _expiration = expiration; 25 } 26 27 /// <summary> 28 /// 构造方法。 29 /// </summary> 30 /// <param name="minute">过期的分钟</param> 31 public DateTimeExpirationPolicy(uint? minute) 32 { 33 _expiration = TimeSpan.FromMinutes((double)minute); 34 } 35 36 /// <summary> 37 /// 是否过期。 38 /// </summary> 39 public bool IsExpired() 40 { 41 return (DateTime.Now - _start) > _expiration; 42 } 43 } 44 }
每隔6小时进行一次垃圾清理
1 var lockManager = BootstrapService.Current.Container.GetInstance<ILockManager>(); 2 var timer = new Timer(state => 3 { 4 lockManager.ReleaseExpiredLocks(); 5 }, null, 1000 * 60 * 6, 1000 * 60 * 6);
备注
早上来的路上想到一个思路可以避免6小时清理一下垃圾,就是使用环形字典,找个时间我试试。
注意:6小时清理一次垃圾,并不代表6小时才过期的。