• 集群环境中实现.Net分布式锁
  • 王帆 发表于 2017/3/1 10:53:00 | 分类标签: 分布式锁 集群环境 死锁
  • 这篇文章要从去年年末一个多搞会的晚上说起,那是一个夜黑风高的晚上,公司应该没有几个人在啦,我司一技术男悠悠的走到我的背后,突然一句:“还没走啊?”!“我日,吓死我啦,你也没走啊”。此同事现在已被裁员,走啦,当晚他问我啦一个问题,至此时也没有机会告知,今天我就在这里就简单描述下他当时的问题,其实实现起来简单的不值一提,不过任何一个简单的问题往往都会有很多中解决方案,探索找到最佳的解决方案,然后把细节做好,那就是技术的精髓与乐趣所在。我这里只抛砖一下,希望能给我老同事一个思路。

    回到问题

    首先有如下一张表,字段有IsBuyed(0:未使用,1:已使用),ProductNo:产品编号,Count:使用次数。
    就是针对这张表做需求扩展的。

    1、每次请求过来,都随机拿到一个未使用过的产品编号
           public int GetNo()
            {
                using (IDbConnection conn = GetConn())
                {
                    return conn.ExecuteScalar<int>("select top 1 ProductNo from  AStore where isBuyed=0 order by newid()");
                }
            }

    2、每次请求过来,即为使用产品一次,使用未使用过的产品一次需产品的IsBuyed=1 , Count=Count+1 。
     public bool MinusStore(int no)
            {
                using (IDbConnection conn = GetConn())
                {
                    return conn.Execute("update AStore set isBuyed=1 , [count]=[count]+1 where productNo=" + no) > 0;
                }
            }

    3、写一个接口,部署在集群环境中,模拟请求3秒内一万个请求,来消费表中只有10个的产品,最终结果为产品不能被多次使用,如果存在多次使用则产品的count将大于1,即为失败。同学如果你看到啦,问题我给你复原的跟你说的没多少出入吧?

    .Net实现分布式锁


    解决问题我就一步步来递进,慢慢深入,直至痛楚!!首先我把同事操作数据上面的2个方法先贴出来。
    public bool MinusStore(int no)
            {
                using (IDbConnection conn = GetConn())
                {
                    return conn.Execute("update AStore set isBuyed=1 , [count]=[count]+1 where productNo=" + no) > 0;
                }
            }      public int GetNo()
            {
                using (IDbConnection conn = GetConn())
                {
                    return conn.ExecuteScalar<int>("select top 1 ProductNo from  AStore where isBuyed=0 order by newid()");
                }
            }      

    初涉茅庐的同学可能会这样写接口。

     public JsonResult Using()
            {
                //获取未使用的产品编号
                var no = data.GetNo();
                if (no != 0)
                {
                    //使用此产品
                    data.MinusStore(no);                
                    return Json(new { success = true, ip = Request.ServerVariables.Get("Local_Addr").ToString() + " : " + HttpContext.Request.Url.Port }, JsonRequestBehavior.AllowGet);
                }
                else
                {
                    return Json(new { success = false, ip = Request.ServerVariables.Get("Local_Addr").ToString() + " : " + HttpContext.Request.Url.Port }, JsonRequestBehavior.AllowGet);
                }
            }
    单机部署,1万个请求过来下啊。下面我们看看数据库的结果是什么?下面是3次实现结果。每执行一次,执行一下下面的脚本。
    select * from [dbo].[AStore]
    update AStore set isbuyed=0,count=0

    由结果可以看出,单机部署接口的情况下,还使一些产品被多次消费,这很显然不符合同学的要求。

    那么我们进一步改进这个接口,使用单机锁,锁此方法,来实现此接口,如下。
     public JsonResult Using()
            {
                //锁此操作
                string key = "%……¥%¥%77123吗,bnjhg%……%……&orderno:" ;
                lock (key)
                {
                    //获取未使用的产品编号
                    var no = data.GetNo();
                    if (no != 0)
                    {
                        //使用此产品
                        data.MinusStore(no);
                        return Json(new { success = true, ip = Request.ServerVariables.Get("Local_Addr").ToString() + " : " + HttpContext.Request.Url.Port }, JsonRequestBehavior.AllowGet);
                    }
                    else
                    {
                        return Json(new { success = false, ip = Request.ServerVariables.Get("Local_Addr").ToString() + " : " + HttpContext.Request.Url.Port }, JsonRequestBehavior.AllowGet);
                    }
                }
            }
    单机部署此接口,1000个请求来测试此接口
    结果如下:

    哇塞,貌似同事的问题解决啦,哈哈,同事不急,这只是单机部署下的结果,如果把这个接口集群部署的话是什么结果呢?

    使用nginx做集群部署,搞5个站点做测试,对得起吗,同事?
     upstream servers{
           server 192.168.10.150:9000 weight=1; 
           server 172.18.11.79:1112 weight=1;
           server 192.168.10.150:1114 weight=1;
           server 192.168.10.150:1115 weight=1;
           server 192.168.10.150:1116 weight=1;  
     }
     server{
          keepalive_requests 1200;
          listen 8080;
          server_name abc.nginx3.com;
          location ~*^.+$ { 
              proxy_pass http://servers;
            }
    }


    再来看此接口运行的结果。结果如下:
    由图可以看出,站点部署的集群对的住你,结果可令我们不满意啊,显然一个产品还是存在多次消费的情况,这种锁对集群部署无用,并且还要排队,性能也跟不上来。我们来进一步改写这个接口。如下:
    public JsonResult Using()
            {
                //锁此操作
                string key = "%……¥%¥%77123吗,bnjhg%……%……&+orderno";
                lock (key)
                {
                    //获取未使用的产品编号
                    var no = data.GetNo();
                    //单号做为key插入memcached,值为true。
                    var getResult = AMemcached.cache.Add("Miaodan_ProNo:" + no, true);
                    if (getResult)
                    {
                        //使用此产品
                        data.MinusStore(no);
                        return Json(new { success = true, ip = Request.ServerVariables.Get("Local_Addr").ToString() + " : " + HttpContext.Request.Url.Port }, JsonRequestBehavior.AllowGet);
                    }
                    else
                    {
                        //此产品已使用过
                        return Json(new { success = false, ip = Request.ServerVariables.Get("Local_Addr").ToString() + " : " + HttpContext.Request.Url.Port }, JsonRequestBehavior.AllowGet);
                    }               
                }
    在集群下跑此接口看结果,结果如下。
    功能实现,同事可以安息啦。不过这里还有很多优化,和分布式锁带来的弊端,比如一单被分布式锁,锁住业务即便后续算法没有使用该产品,怎么优雅的释放锁,怎么解决遇到已经使用过的产品后再此分配新资源等等,当然也有其他一些实现方案,比如基于redis,zookeeper实现的分布式锁,我这里就不说明啦。同事,你好自珍重,祝多生孩子,多挣钱啊。

  • 请您注意

    ·自觉遵守:爱国、守法、自律、真实、文明的原则

    ·尊重网上道德,遵守《全国人大常委会关于维护互联网安全的决定》及中华人民共和国其他各项有关法律法规

    ·严禁发表危害国家安全,破坏民族团结、国家宗教政策和社会稳定,含侮辱、诽谤、教唆、淫秽等内容的作品

    ·承担一切因您的行为而直接或间接导致的民事或刑事法律责任

    ·您在编程中国社区新闻评论发表的作品,本网站有权在网站内保留、转载、引用或者删除

    ·参与本评论即表明您已经阅读并接受上述条款

  • 感谢本文作者
  • 作者头像
  • 昵称:王帆
  • 加入时间:2013/6/13 0:00:00
  • TA的签名
  • 这家伙很懒,虾米都没写
  • +进入TA的空间
  • 以下内容也很赞哦
分享按钮