目錄
一、業務背景:
2018 年,是自如充滿機遇與挑戰的一年,在年初成功獲得了 40 億 A 輪融資,自如客突破 150 萬,管理資產直逼 100 萬間。
快速擴張的同時,電商項目也隨之發力。優品事業部週會是老闆也會參加的例會,可謂內部的重點創業項目之一。
最近嘗試一種新的分銷模式,根據自如大體量的管家進行線下線上推廣,通過活動的形式讓用戶領取優惠券,通過券促成單後管家可進行分傭。管家之間會在所屬大區,所屬小組分別進行排名,進行排名再獎勵激發推廣熱情。想想還是蠻誘人的,畢竟赤果果的錢呀。
這個項目有幾個明顯需要重點關注的點
- 涉及到錢的準確性:不發超不少發
- 數據信息的及時性:分銷金額與排名的實時查看
- 動態性:管家因爲調區調組帶來的數據實時變動問題
快速迭代開發是php的優勢呀,但是涉及到錢的問題,真的因該慎之又慎。最終選擇了lua+redis的技術方案在服務端運行,保證活動的穩定性。
二.技術實現
1.爲什麼是lua?
Redis 從 2.6.0 版本起,,通過內置的 Lua 解釋器,也已開始支持 Lua 腳本的執行。
我們可以更加得心應手地使用或擴展 Redis,特別是在高併發場景下 Lua 腳本提供了更高效、可靠的解決方案。
好處:
- 減少網絡開銷:本來5次網絡請求的操作,可以用一個請求完成,原先5次請求的邏輯放在redis服務器上完成。使用腳本,減少了網絡往返時延。
- 原子操作:Redis會將整個腳本作爲一個整體執行,中間不會被其他命令插入。
- 複用:客戶端發送的腳本會永久存儲在Redis中,意味着其他客戶端可以複用這一腳本而不需要使用代碼完成同樣的邏輯。
2.設計方案:
分享推廣環節不做過多敘述,重點分析一下訂單相關流程,簡化如圖下所示,主要涉及三個部分。
因爲我們使用的有贊雲,做分銷活動自然也繞不開訂單的同步回傳的問題,
當用戶在有贊下單之後會以http的形式進行通知。接受時這裏做了異步處理,把消息推入redis隊列中等待處理,並返回成功結果給有贊。
同時再掛起進程對隊列中的訂單進行異步處理,通過Supervisor來監控進程的存活,發現訂單任務堆積,可動態增加進程。
技術設計流程圖如下,
【用戶下單】與【訂單查分】處理示意圖
拆分訂單時,當發現是發貨操作,通過基於elastic-job的分佈式隊列,寫到作業當中。
把訂單拆分邏輯與分傭計算異步解耦,只要成功扔到作業中即可,業務不必依賴於分傭邏輯的結果。
紅色框中就是進行【分傭計算】的後續銜接部分,通過這個節點,分佈式作業會對成功寫入隊列的任務進行執行,輪訓執行與異常重試,當發現執行失敗是會進行重試(10次)
通過redis+lua的形式保證數據一致性與原子性
通過redis-zset等結構保證排序查詢性能問題
(注意:分傭邏輯做了冪等操作,防止補償重試時發生超發現象)
3.lua+redis的使用
Redis 提供了 EVAL(直接執行腳本) 和 EVALSHA(執行 SHA1 值的腳本) 這兩個命令,可以使用內置的 Lua 解析器執行 Lua 腳本。語法格式爲:
EVAL script numkeys key [key …] arg [arg …]
EVALSHA sha1 numkeys key [key …] arg [arg …]
參數說明:
script / sha1:EVAL 命令的第一個參數爲需要執行的 Lua 腳本字符,EVALSHA 命令的一個參數爲 Lua 腳本的 SHA1 值
numkeys:表示 key 的個數
key [key …]:從第三個參數開始算起,表示在腳本中所用到的那些 Redis 鍵(key),這些鍵名參數可以在 Lua 中通過全局數組 KYES[i] 訪問
arg [arg …]:附加參數,在 Lua 中通過全局數組 ARGV[i] 訪問
lua+redis在php中的使用
$script = $this->luaScript();//以字符串的形式存儲lua腳本
$data = array('key1','key2','value1');//在lua腳本中通過KEYS[1] KEYS[2], ARGV[1]的順序獲取
$keylen = 2;//表示$data中前兩個值是key,表示key的個數
$res = static::$redis->eval($script ,$data,$keylen);
三.策略模式
在軟件開發中也常常遇到類似的情況,實現某一個功能有多個途徑,此時可以使用一種設計模式來使得系統可以靈活地選擇解決途徑,也能夠方便地增加新的解決途徑。
策略模式就是用來封裝算法的,但在實踐中,我們發現可以用它來封裝幾乎任何類型的規則,只要在分析過程中聽到需要在不同時間應用不同的業務規則,就可以考慮使用策略模式處理這種變化的可能性。
而我們這個分銷計算場景中,有初始化create場景,有累加incr場景,有delete場景,甚至還有動態調配所有值的場景,並且使整個體系覈銷能對的上。那麼策略模式就很適合了。
<?php
class ProfitDataLogic
{
public static $data;
public static $redis;
public function __construct( $strategyName = '' , array $data = array() )
{
if(empty(static::$redis)){
static::$redis= Yii::$app->redis;
}
$this->strategyName = $this->file_pre.ucwords(strtolower($strategyName)).'Logic';
}
public function deal()
{
try{
$namespace = '\\' . __NAMESPACE__ . '\\';
$class_name = $namespace.$this->strategyName;
$exists = class_exists($class_name);
if (!$exists) {
return $this->fail_return('非法訪問,該策略不存在');
}
$strategyReflection = new \ReflectionClass($class_name);
$strategy = $strategyReflection->newInstance();
return $strategy->run();
}catch(\ReflectionException $e){
return $e->getMessage();
}
}
}
Interface ProfitStrategyLogic
{
/**
* 任務執行
* @return mixed
*/
public function run();
/**
* 存儲lua運行腳本
* @return mixed
*/
public function +luaScript();
/**
* 處理拼接lua所需key值與value值
* $param $data
* @return mixed
*/
public function dealData();
}
class ProfitLuaAddLogic extends ProfitDataLogic implements ProfitStrategyLogic
{
public function run()
{
$deal = $this->dealData();
static::$redis->eval($this->luaScript(),$deal['data'],$deal['key_length']);
}
public function luaScript()
{
$luaScript = <<<LUA
--新增lua腳本邏輯
LUA;
return $luaScript;
}
public function dealData()
{
//組裝新增lua腳本邏輯所需key,value
}
}
class ProfitLuaIncrLogic extends ProfitDataLogic implements ProfitStrategyLogic
{
public function run()
{
$deal = $this->dealData();
static::$redis->eval($this->luaScript(),$deal['data'],$deal['key_length']);
}
public function luaScript()
{
$luaScript = <<<LUA
--累加lua腳本邏輯
LUA;
return $luaScript;
}
public function dealData()
{
//組裝incr新增場景所需要數據
}
}
$logic = new ProfitDataLogic('add',$order_info);//執行添加操作
$logic = new ProfitDataLogic('incr',$order_info);//執行累加操作
$re = $logic->deal();
策略模式提供了可以替換繼承關係的辦法,繼承可以處理多種算法或行爲
優點:
1.規範性可標準拓展:策略模式提供了可以替換繼承關係的辦法,繼承可以處理多種算法或行爲。
2.策略模式提供了可以替換繼承關係的辦法,繼承可以處理多種算法或行爲。
3.使用策略模式可以避免使用多重條件轉移語句。
缺點:
1.客戶端必須知道所有的策略類,並自行決定使用哪一個策略類。
2.策略模式造成很多的策略類,每個具體策略類都會產生一個新類。
四.問題答惑
1、php中如何調試lua腳本
在php中執行的lua腳本其實是通過redis的eval函數嵌入原子執行的,單純的打印或者寫日誌的方法都是行不通的,所以說調試起來還比較麻煩。好在php的redis擴展中也加了異常捕獲,可以通過:
var_export(static::$redis->getLastError());
來返回腳本中發生的錯誤。
2.爲什麼不使用redis事務
redis中的事務並不像mysql中那麼完美,只是簡單的保證了原子性。
事務的實現原理是把事務中的命令先放入隊列中,當client提交了exec命令後,redis會把隊列中的每一條命令按序執行一遍。如果在執行exec之前事務中斷了或未提交,則統一不執行。
而redis確保正一條script腳本執行期間,其它任何腳本或者命令都無法執行。正是由於這種原子性,script纔可以替代MULTI/EXEC作爲事務使用。
官網文檔上有這樣一段話:
A Redis script is transactional by definition, so everything you can do with a Redis transaction, you can also do with a script, and usually the script will be both simpler and faster.
3.如何保證原子性
因爲Redis是單線程。Redis本身提供的所有API都是原子操作,所以Redis中的事務其實是要保證批量操作的原子性
舉個例子,賣商品
$goods_num = $redis->get('goods_num');
if($goods_num<1){
return false;
}else{
//todo sale logic
$goods_num = $redis->set('goods_num',0);
}
當高併發時,有可能就會出現redis還沒有set成功0,新的訪問又進來了,結果執行了兩次售賣邏輯,但腳本中的get set是原子性操作時則不會發生這樣的問題的。
4.如何保證數據一致性
爲了達到數據最終一致性,我們引入了基於elastic-job的分佈式作業有失敗重試機制,生成一個全局唯一的外部訂單號,當某分傭發放失敗,就會放回任務隊列,使得有機會進行發放重試,當然這一切都需要 API 做冪等處理。這樣就保證了不超發也不會少發。
public function luaScript()
{
$luaScript = <<<LUA
local mideng = redis.call('SISMEMBER', KEYS[13],ARGV[10])
if mideng >0 --如果已經做過處理,返回true表示執行成功過
then
return true
end
local guanjia_hash = redis.call('hget', KEYS[1],'name')
if not guanjia_hash
then
--初始化hash
redis.call('HMSET', KEYS[1], KEYS[9], ARGV[9],KEYS[10], ARGV[1],KEYS[16], ARGV[2],KEYS[17], ARGV[3])
else
--管家調區刷新信息
redis.call('HMSET', KEYS[1] , KEYS[10], ARGV[1],KEYS[16], ARGV[2],KEYS[17], ARGV[3])
--金額累加
redis.call('HINCRBY', KEYS[1],KEYS[9],ARGV[9])
end
--冪等
redis.call('SADD', KEYS[13],ARGV[10])
--初始化管家小組排名
redis.call('ZINCRBY',KEYS[2],ARGV[9],ARGV[1])
--初始化管家大區排名
redis.call('ZINCRBY',KEYS[3],ARGV[9],ARGV[1])
--更新小組總排名
LUA;
return $luaScript;
}
需要注意 Lua 腳本執行過程並不是事務的,腳本中的操作命令在執行時是有先後順序的,當某個操作執行失敗時不會回滾已經執行成功的操作,它的原子性是通過單線程模型實現。
客單價 = 總金額/購買商品數量 (170元)
動銷率=有銷量的商品/在線銷售的商品 (95%)
成單率=訂單數量 / 累計進店客戶 (1.8%)
短信召回率:發券召回人數(10%)
參考文章:
https://redis.io/commands/eval
https://blog.csdn.net/fangjian1204/article/details/50585080
https://www.cnblogs.com/yanghuahui/p/3697996.html
Redis 的操作爲什麼是的原子性的詳解:https://blog.csdn.net/qq646040754/article/details/81066805