• Orleans初战(用分布式解决高并发购物场景)


    首先我们来定义这样一个场景

        商店有10种商品,每种商品有100件库存。现在有20万人来抢购这些商品。

    OK,那么问题来了。要怎样保证商品不会超卖……(要知道可能会出现20个人同时买A商品(或者更糟糕,毕竟后边20万的大军,随时可能把商店变成废墟),怎样保证A商品的数量绝对安全)

    按照大部分系统的解决方案是这样的

      收到请求放入队列,然后对队列顺序处理,这样就避免了系统被瞬间挤爆而且不会超卖。

    这种处理方式装换成现实场景是这样的:客户到商店先领号,不管买什么商品,都要排队,然后一个一个买,直到所有的处理完

    这个是不是弱爆了………………

    这个解决方案也就相当于一个售卖窗口,大家在那排队买,你能受得了吗?

     

    先看看现实商店怎样解决的(存在即合理):客户太多就加窗口呗,多雇员工,粗暴又简单的解决了问题(当然大家还是要排队,但是不是一个队了,缓解了压力提高了速度哦,老板赚到了更多的钱)

    Orleans闪亮登场…………

    首先我要多开几台服务器来处理客户的请求,怎样分配呢,要知道我的商品库存数量必须保证安全,如果几台服务器操作一个商品那我们要想办法做到对象的绝对同步(joab开始也是这样想的,后来我才知道是我想多了),要知道加的服务器处理数据同步的消耗实在太大得不偿失啊(线程之间的数据安全使用线程锁我们都闲消耗大,这个夸服务器就更别说了)……

    换个思路:加几台服务器,每台服务器买不同的商品,例如:1号服务器卖a/b两种商品,2号服务器卖c/d两种商品…………以此类推,问题解决了……

    客户消息说买a商品,直接到1号服务器排队,买c商品就去2号服务器排队,(当然这里服务器也要多线程,一样的解决原理,a商品x线程排队,b商品y线程排队)

    好了,从场景到解决办法都出来了,现在要实现:

    照例我们开始搭建环境(事例我就简单三层了,现实项目大家自己根据项目自己发挥啊)

    访问关系:

    Orleans.Samples.HostSilo就是个控制台应用程序,用于启动Orleans服务(Silo的启动)也就相当于售货的窗口,不同服务器启动Orleans.Samples.HostSilo来处理排队的请求(配置我就先不贴出来了,很多地方有)

    Orleans.Samples.Grains你可以理解为商品,它在需要在窗口售卖

    Orleans.Samples.StorageProvider这个怎么说呢,首先Orleans.Samples.Grains是运行在服务端的而且可以是有状态的,我们怎么来管理他的状态,StorageProvider就对Grain的状态做了扩展(本例我就那这个状态来做商品数据的读写,并且对商品扣库存时也是直接对本Grain的state进行操作)

     其它的几个我就不讲了大家一看就知道是什么了。

    关键代码

    一、GoodsStorgeProvider

    public class GoodsStorgeProvider : IStorageProvider
        {
            public Logger Log
            {
                get; set;
            }
    
            public string Name
            {
                get; set;
            }
    
            public Task ClearStateAsync(string grainType, GrainReference grainReference, IGrainState grainState)
            {
                return TaskDone.Done;
            }
    
            public Task Close()
            {
                return TaskDone.Done;
            }
    
            public Task Init(string name, IProviderRuntime providerRuntime, IProviderConfiguration config)
            {
                this.Name = nameof(GoodsStorgeProvider);
                this.Log = providerRuntime.GetLogger(this.Name);
    
                return TaskDone.Done;
            }
    
            public async Task ReadStateAsync(string grainType, GrainReference grainReference, IGrainState grainState)
            {
                Console.WriteLine("获取商品信息");
                var goodsNo = grainReference.GetPrimaryKeyString();
                using (var context = EntityContext.Factory())
                {
                    grainState.State = context.GoodsInfo.AsNoTracking().FirstOrDefault(o => o.GoodsNo.Equals(goodsNo));
                }
                await TaskDone.Done;
                
            }
    
            public async Task WriteStateAsync(string grainType, GrainReference grainReference, IGrainState grainState)
            {
                var model = grainState.State as GoodsInfo;
                using (var context = EntityContext.Factory())
                {
                    var entity = context.GoodsInfo.FirstOrDefault(o => o.GoodsNo.Equals(model.GoodsNo));
                    entity.Stock = model.Stock;
                    await context.SaveChangesAsync();
                }
            }
        }

     前边说过了Grain是有状态的,我定义了GoodsStorgeProvider管理商品的状态,商品的读取我是直接从数据库读出然后赋值个它的State,那么知道这个Grain被释放,这个State将一直存在,并且唯一,写入我就直接对商品的Stock进行了赋值并且保存到数据库(售卖商品,变更的就只有商品的数量)

    二、GoodsInfoGrain

        [StorageProvider(ProviderName = "GoodsStorgeProvider")]
        public class GoodsInfoGrain : Grain<GoodsInfo>, IGoodsInfoGrain
        {
            public Task<List<GoodsInfo>> GetAllGoods()
            {
                using (var context = EntityContext.Factory())
                {
                    return Task.FromResult(context.GoodsInfo.AsNoTracking().ToList());
                }
            }
    
            public async Task<bool> BuyGoods(int count, string buyerUser)
            {
                Console.WriteLine(buyerUser + ":购买商品--" + this.State.GoodsName + "    " + count + "");
    
                if (count>0 && this.State.Stock >= count)
                {
                    this.State.Stock -= count;
                    OrdersInfo ordersInfo = new OrdersInfo();
                    ordersInfo.OrderNo = Guid.NewGuid().ToString("n");
                    ordersInfo.BuyCount = count;
                    ordersInfo.BuyerNo = buyerUser;
                    ordersInfo.GoodsNo = this.State.GoodsNo;
                    ordersInfo.InTime = DateTime.Now;
                    using (var context = EntityContext.Factory())
                    {
                        context.OrdersInfo.Add(ordersInfo);
                        await context.SaveChangesAsync();
                    }
                    await this.WriteStateAsync();
                    Console.WriteLine("购买完成");
                    return await Task.FromResult(true);
                }
                else
                {
                    Console.WriteLine("库存不足--剩余库存:" + this.State.Stock);
                    return await Task.FromResult(false);
                }
            }
            
            
        }

    我们有10种商品所以也就是会有10个Grain的实例保存在服务端,具体哪个Grain的实例代码那种商品我们可以根据商品编号来划分,GoodsInfoGrain继承自IGoodsInfoGrain,IGoodsInfoGrain继承自IGrainWithStringKey,IGrainWithStringKey的实例化需要一个string类型的key,我们就用商品的编号作为这个Grain实例的Key

    这里我指定此Grain的StorageProvider为GoodsStorgeProvider,那么当Grain被实例化的时候GoodsStorgeProvider也被实例化并且执行ReadStateAsync,那么这个商品就在服务端存在了,不用每次去数据库读而是一直存在服务端

    这里我们服务端是不需要特意人为的进行排队处理,Grain的实例我们可以理解为是线程安全的(微软并不是使用线程锁来做的这样做太浪费资源,有兴趣的鞋童可以研究下源码,这对你编程水平的提高很有作用)所以不会出现对象被同时调用,而是顺序调用。

    客户端调用:

           var grain = GrainClient.GrainFactory.GetGrain<IGoodsInfoGrain>(goods.GoodsNo);
                bool result = grain.BuyGoods(count, buyerUser).Result;
                if (result)
                {
                    Addmsg(buyerUser + "--购买商品" + goods.GoodsName + "    " + count + "");
                }
                else
                {
                    Addmsg(buyerUser + "--购买商品" + goods.GoodsName + "    库存不足");
                }

    大家可以看到,GrainClient.GrainFactory.GetGrain<IGoodsInfoGrain>(goods.GoodsNo)就是告诉服务端需要用哪个grain执行我的操作,然后使用这个grain去调用BuyGoods方法购买商品不需要告诉服务端商品的编号,只需要买几个,购买人是谁就可以了,因为grain在实例化(当然还是那句话,Grain是有状态的不需要每次实例化,)时就已经定了它是哪种商品。

    OK,源码地址:https://github.com/zhuqingbo/Orleans.Samples

     今天举例的这个场景是有破绽的,例如:有20万人都是来买一种商品的,那么就意味着只有一个服务器忙到死,但是其他的服务器都是空闲的,就像我商场雇了100个销售人员,只有一个人在卖东西其他销售都没事,顾客要排队很久…………这个是不允许出现的!!!我们应该怎么解决?这个解决办法我会在下次的事例中和大家分享,大家不妨在留言中提出一些自己的解决办法,我们一起研究研究

  • 相关阅读:
    RabbitMQ链接不上异常
    设计模式之禅之六大设计原则-迪米特原则
    <十二>面向对象分析之UML核心元素之节点和设备
    <十一>面向对象分析之UML核心元素之组件
    <十>面向对象分析之UML核心元素之关系
    <九>面向对象分析之UML核心元素之设计类,类,属性,方法,可见性
    <八>面向对象分析之UML核心元素之分析类
    <七>面向对象分析之UML核心元素之包
    <六>面向对象分析之UML核心元素之业务实体
    Spring Cloud(七):使用SVN存储分布式配置中心文件和实现refresh
  • 原文地址:https://www.cnblogs.com/joab/p/5657851.html
Copyright © 2020-2023  润新知