常见误用场景:在订单支付环节中,为了防止用户不小心多次点击支付按钮而导致的订单重复支付问题,我们用 lock(订单号) 来保证对该订单的操作同时只允许一个线程执行。
这样的想法很好,至少比 lock(处理类的private static object)要好,因为lock订单号想要的效果是只锁当前1个订单的操作,而如果lock静态变量,那就是锁所有的订单,就会导致所有的订单进行排队,这显然是不合理的。
那么本文开篇说的lock(订单号)的做法可以实现想要的效果吗?我们先用一些代码来还原使用场景。
如果忽略用户信息及其他验证,那代码差不多是这样:
1 public ActionResult PayOrder(string orderNumber) 2 { 3 lock (orderNumber) 4 { 5 //订单支付,消息通知等耗时的操作 6 } 7 return View("Success"); 8 }
这样的代码看起来好像没有什么问题,对于lock关键字,MSDN上面包括能够百度到的资料,好像都是说建议不要使用lock(string),而原因都是同一个。以下这段话摘自MSDN关于lock字符串的建议:
由于进程中使用同一字符串的任何其他代码将共享同一个锁,所以出现 lock(“myLock”) 问题。
这句话隐藏了一个巨大的机关,那就是“同一字符串”。
什么叫“同一字符串”?请看代码:
static void Main(string[] args) { var str1 = "abc"; var str2 = "abc"; }
请问上面的str1和str2是同一字符串吗?答案是YES。
再看:
static void Main(string[] args) { var str1 = "abc" + 123; var str2 = "abc" + 123; }
上面的str1和str2还是同一字符串吗?答案就是NO了。
好了,再回到我们订单支付的问题上面来。在我们的代码中, lock(orderNumber) ,当用户手滑一不小心多点了几次,请问每次进入这个action的orderNumber是同一字符串吗?答案是NO。这就是说
上面处理订单的代码实际上并没有起到任何lock的作用。
实际上,字符串比较分两种,请看代码:
static void Main(string[] args) { var str1 = "abc" + 123; var str2 = "abc" + 123; Console.WriteLine(str1 == str2); Console.WriteLine(object.ReferenceEquals(str1, str2)); }
上面的代码第一行输出True,第二行输出False。相信不用我解释你也明白MSDN所说的“同一字符串”了。
最后,再分享一个我们项目中用来解决lock(订单号)的方案。
调用方法:
public ActionResult PayOrder(string orderNumber) { Locker.Run(orderNumber, () => { //订单支付,消息通知等耗时的操作 }); return View("Success"); }
用到的Locker类:
1 public class Locker 2 { 3 private const int ExpireMinutes = 10; 4 5 private static readonly Timer _timer; 6 private static readonly Dictionary<string, LockObj> _dict = new Dictionary<string, LockObj>(); 7 8 static Locker() 9 { 10 _timer = new Timer(60000); 11 _timer.Elapsed += (s, e) => 12 { 13 RemovedExired(); 14 }; 15 _timer.Start(); 16 } 17 18 public static void Run(string key, Action action) 19 { 20 LockObj lockObj = null; 21 lock (_dict) 22 { 23 if (!_dict.ContainsKey(key)) 24 { 25 _dict[key] = new LockObj(); 26 } 27 lockObj = _dict[key]; 28 lockObj.Time = DateTime.Now; 29 } 30 lock (lockObj) 31 { 32 action(); 33 } 34 } 35 36 public static void RemovedExired() 37 { 38 lock (_dict) 39 { 40 var keys = _dict.Where(x => x.Value.IsExpired()).Select(x => x.Key).ToList(); 41 foreach (var key in keys) 42 { 43 _dict.Remove(key); 44 } 45 } 46 } 47 48 private class LockObj 49 { 50 public DateTime Time { private get; set; } 51 52 public bool IsExpired() 53 { 54 return this.Time < DateTime.Now.AddMinutes(-ExpireMinutes); 55 } 56 } 57 }
总结
lock(字符串)其实最大的用处就是类似锁定当前订单的操作,lock一个常量字符串就没有多大意义,正如MSDN所说,不推荐使用。