lock订单号

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了lock订单号相关的知识,希望对你有一定的参考价值。

 常见误用场景:在订单支付环节中,为了防止用户不小心多次点击支付按钮而导致的订单重复支付问题,我们用 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所说,不推荐使用。

以上是关于lock订单号的主要内容,如果未能解决你的问题,请参考以下文章

php文件锁解决少量并发问题

生成唯一订单号 (支持每秒1000个并发)

MySQL next-key lock 加锁范围总结

锁的总结

锁的总结

java并发编程的艺术——第五章总结(Lock锁与队列同步器)