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個銷售人員,只有一個人在賣東西其他銷售都沒事,顧客要排隊很久…………這個是不允許出現的!!!我們應該怎么解決?這個解決辦法我會在下次的事例中和大家分享,大家不妨在留言中提出一些自己的解決辦法,我們一起研究研究

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM