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个销售人员,只有一个人在卖东西其他销售都没事,顾客要排队很久…………这个是不允许出现的!!!我们应该怎么解决?这个解决办法我会在下次的事例中和大家分享,大家不妨在留言中提出一些自己的解决办法,我们一起研究研究

原文地址:https://www.cnblogs.com/joab/p/5657851.html