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