背景
隨著達(dá)達(dá)業(yè)務(wù)的快速發(fā)展,產(chǎn)品上經(jīng)常需要對(duì)APP的邏輯進(jìn)行更精準(zhǔn)快速的變更,最初通過發(fā)布新版本的方式來調(diào)整邏輯,由于APP版本更新覆蓋需要一定的周期,這樣做的效果不是很理想。后面我們采用了友盟在線參數(shù)來對(duì)一些參數(shù)進(jìn)行動(dòng)態(tài)配置,但友盟的在線參數(shù)只是一個(gè)簡單的key-value配置,無法為配置的key附帶更多的業(yè)務(wù)邏輯(比如限定城市和用戶),并且配置變更以后也無法立即通知APP,因此我們?cè)O(shè)計(jì)了達(dá)達(dá)APP配置系統(tǒng)。
目的
從產(chǎn)品上來說,APP配置就是需要準(zhǔn)確及時(shí)的針對(duì)不同類型的用戶,執(zhí)行不同的業(yè)務(wù)邏輯。在達(dá)達(dá)的業(yè)務(wù)場景下,我們經(jīng)常會(huì)對(duì)APP按城市和用戶的維度做一些配置。例如:
控制一個(gè)功能入口的顯示和隱藏
比如我們會(huì)對(duì)某些優(yōu)質(zhì)配送員開放達(dá)達(dá)商城入口,讓他們可以購買達(dá)達(dá)裝備;而有些配送員則不開放。
配置APP中的文案
根據(jù)不同的運(yùn)營策略,我們經(jīng)常要實(shí)時(shí)調(diào)整我們APP內(nèi)的文案。
下面左圖是顯示達(dá)達(dá)商城功能,右圖是隱藏達(dá)達(dá)商城功能:
從技術(shù)上來講,需要滿足如下要求:
實(shí)時(shí)性
服務(wù)端的配置發(fā)生更新,必須要在盡可能短的時(shí)間內(nèi)通知APP進(jìn)行配置的變更,尤其對(duì)于需要盡快生效的配置更為重要,比如線上服務(wù)器壓力比較大,需要及時(shí)通知APP增加刷新訂單的等待時(shí)間以降低服務(wù)器壓力,如果該配置通知到APP的時(shí)間比較長,那這條配置也就沒有什么意義了,所以實(shí)時(shí)性也是非常重要的一個(gè)要求。同時(shí)配置變更通知也需要對(duì)用戶保證透明性,讓用戶在無感知的情況下進(jìn)行配置變更。
高并發(fā)
該系統(tǒng)面向的是達(dá)達(dá)公司所有的APP設(shè)備,設(shè)備數(shù)量是非常多的,如果配置發(fā)生更新,必須要考慮大量設(shè)備同時(shí)訪問服務(wù)端來更新配置對(duì)服務(wù)器造成的壓力,以及在這種高并發(fā)訪問下如何保證服務(wù)的可用性。
低客戶端流量使用
因?yàn)樵撓到y(tǒng)主要面向APP端,流量因素也是需要考慮的一個(gè)問題,為了節(jié)省客戶端流量,APP獲取更新時(shí)不需要每次更新都獲取全部配置,只需要獲取在上次更新后發(fā)生變更的字段,需要支持配置的增量刪除/新增/修改等功能
設(shè)計(jì)及實(shí)現(xiàn)
根據(jù)需求我們?cè)O(shè)計(jì)并實(shí)現(xiàn)了達(dá)達(dá)APP實(shí)時(shí)配置系統(tǒng),包含服務(wù)端,Android端和iOS端。
APP配置系統(tǒng)的設(shè)計(jì)如下圖所示:
服務(wù)端的架構(gòu)主要分為數(shù)據(jù)庫層/緩存層(本地緩存層+Redis緩存層)/配置計(jì)算層
配置信息存儲(chǔ)在數(shù)據(jù)庫中
使用Redis存儲(chǔ)緩存配置信息減小直接讀取數(shù)據(jù)庫的壓力,同時(shí)使用本地緩存與Redis同步,配置直接從本地緩存讀取,減少網(wǎng)絡(luò)IO次數(shù),提高系統(tǒng)性能,減少請(qǐng)求響應(yīng)時(shí)間
配置計(jì)算層主要用來進(jìn)行配置的篩選工作,根據(jù)不同的請(qǐng)求條件篩選對(duì)應(yīng)配置,并實(shí)現(xiàn)配置的增量更新等功能
在與APP的交互方面,在安卓端我們主要使用了HTTP+推送的方案,每當(dāng)配置發(fā)生更新,會(huì)通過信鴿推送發(fā)送一條透傳消息給APP,APP收到推送后,執(zhí)行HTTP請(qǐng)求拉取配置。iOS端因?yàn)锳PNS推送的字節(jié)限制,以及APP離線時(shí)無法控制讓推送內(nèi)容不在通知欄顯示等原因,所以采用了Socket長連接方案來實(shí)現(xiàn),APP啟動(dòng)時(shí)會(huì)建立一條長連接通道,每當(dāng)配置發(fā)生更新,服務(wù)端會(huì)主動(dòng)將更新的配置發(fā)送給客戶端。
實(shí)時(shí)性
為滿足實(shí)時(shí)性的要求,我們?cè)瓉泶蛩闶褂猛扑蛠韺?shí)現(xiàn),但是使用推送會(huì)面臨兩個(gè)問題:
推送到達(dá)時(shí)間無法估計(jì)
iOS推送如果應(yīng)用不在線,會(huì)在通知欄展示,無法滿足對(duì)用戶透明的需求。
因?yàn)橐陨蟽蓚€(gè)問題,我們對(duì)iOS使用了長連接來實(shí)現(xiàn)配置的主動(dòng)推送,APP啟動(dòng)后會(huì)建立一條與服務(wù)器端的長連接,并維持心跳,每當(dāng)服務(wù)器發(fā)生配置變更,會(huì)實(shí)時(shí)的通知在線的APP更新配置,如果APP離線會(huì)在在線后收到配置更新的推送,保證配置更新的通知不會(huì)在通知欄展示,既解決了實(shí)時(shí)性的要求,也滿足了對(duì)用戶透明的需要。
高并發(fā)
為了解決前面所描述的短時(shí)間高并發(fā)的問題,我們主要從兩個(gè)方面來解決這個(gè)問題:
APP端:收到配置變更的通知后,會(huì)隨機(jī)延遲0-30s再去服務(wù)器拉取配置,防止瞬間的高并發(fā)訪問導(dǎo)致服務(wù)端異常
服務(wù)端:為了提升服務(wù)端性能,減少網(wǎng)絡(luò)IO的時(shí)間,增加了本地緩存層,數(shù)據(jù)直接從本地緩存存取,本地緩存與Redis保持同步更新,使用Zookeeper的節(jié)點(diǎn)監(jiān)聽功能,當(dāng)Redis發(fā)生修改時(shí)主動(dòng)讓Zookeeper通知本地緩存進(jìn)行配置數(shù)據(jù)的更新同步;
低客戶端流量使用
為了解決節(jié)省流量的問題,我們使用了如下兩個(gè)方案:
使用ProtocolBuf進(jìn)行數(shù)據(jù)傳輸
使用該工具的原因主要有以下兩點(diǎn):
序列化和反序列化性能非常高
序列化后字節(jié)數(shù)少,非常適合移動(dòng)端的數(shù)據(jù)傳輸
基于以上兩個(gè)特點(diǎn),使用該框架可以很好的解決流量和性能的問題。
配置增量更新
所有的配置都帶有一個(gè)版本號(hào),配置變更會(huì)基于版本號(hào)進(jìn)行變動(dòng),APP端會(huì)持有一個(gè)本地的版本號(hào),每次更新請(qǐng)求,在請(qǐng)求服務(wù)端時(shí)會(huì)將APP本地的版本號(hào)提供給服務(wù)端,服務(wù)端根據(jù)APP的本地版本號(hào)與服務(wù)器的版本號(hào)進(jìn)行配置的計(jì)算和合并后將發(fā)生變動(dòng)的配置發(fā)送給客戶端,避免每次將所有配置發(fā)送給客戶端,只要發(fā)送發(fā)生變動(dòng)的配置即可。
服務(wù)端
為了實(shí)現(xiàn)前面所描述的低流量的需要,我們使用了增量更新的方案來減少客戶端的流量消耗,基于版本號(hào)的方式實(shí)現(xiàn)了配置的增量更新,方案如下:
配置版本(增量更新)的設(shè)計(jì)
客戶端保存一個(gè)版本號(hào)用于標(biāo)識(shí)APP當(dāng)前配置的版本,每次APP增量更新配置的時(shí)候會(huì)將這個(gè)版本號(hào)發(fā)送給服務(wù)端,服務(wù)端返回該版本號(hào)之后發(fā)生的所有變動(dòng)
服務(wù)端實(shí)現(xiàn)邏輯:
計(jì)算從版本0開始到最新版本之間的有效配置
計(jì)算從APP版本號(hào)到最新版本之間刪除的配置
將生效配置和刪除配置作配置合并操作,生成最后需要更新的增量配置
配置合并的邏輯如下:
有效配置中有,刪除項(xiàng)中沒有,若版本≤APP的本地版本號(hào),該配置無變動(dòng),不需要通知客戶端
有效配置中有,刪除項(xiàng)中沒有,若版本>APP的本地版本號(hào),該配置有變動(dòng),需要通知客戶端更新配置
有效配置中有,刪除項(xiàng)中也有,配置可能變動(dòng),需要通知客戶端更新配置
有效配置中沒有,刪除項(xiàng)中有,配置項(xiàng)被刪除,需要通知客戶端刪除配置
Android端
設(shè)計(jì)結(jié)構(gòu)圖如下:
要點(diǎn)如下:
使用了推送的方式來保證APP能夠?qū)崟r(shí)更新配置
使用了sqlite來同步更新服務(wù)器的配置,作為客戶的讀取配置的數(shù)據(jù)源
為了安全性和靈活性,使用了本地廣播(LocalBroadcast)的方式來通知前臺(tái)頁面(Activity)實(shí)時(shí)更新界面
核心代碼如下:
ConfigList configList = responseBody.getContentAs(ConfigList.class);
if(!Arrays.isEmpty(configList.getResult())) {
for(Config config : configList.getResult()) {
config.setUserId(HttpInterceptor.getUserId());
Config localConfig = getConfig(config.getParamName());
Intent intent = new Intent();
intent.setAction(action(config.getParamName()));
if(config.isDelete()) {
//本地有,但服務(wù)器配置已被刪除,本地也要被刪除
if(localConfig != null)
db.delete(Config.class
, WhereBuilder.b("paramName","=",
config.getParamName())
.and("userId","=", HttpInterceptor.getUserId()));
intent.putExtra(Extras.CONFIG, config);
}elseif(localConfig == null) {
//本地沒有,則新增一個(gè)配置
db.save(config);
intent.putExtra(Extras.CONFIG, config);
}else{
//本地有,服務(wù)器上配置被修改,本地也要被修改
localConfig.setParamValue(config.getParamValue());
db.update(localConfig,"paramValue");
intent.putExtra(Extras.CONFIG, localConfig);
}
localBroadcastManager.sendBroadcast(intent);
}
}
代碼主要邏輯如下:
如果從服務(wù)端拿到一個(gè)刪除的配置,則刪除本地配置
如果從服務(wù)端拿到一個(gè)新增的配置,則插入本地?cái)?shù)據(jù)庫
否則,則更新本地的配置
最后,發(fā)送配置變更廣播通知相應(yīng)的界面更新UI
iOS端
設(shè)計(jì)結(jié)構(gòu)圖如下:
要點(diǎn)如下:
與服務(wù)端維持長連接,保證配置實(shí)時(shí)更新到客戶端
archive配置信息,優(yōu)先從本地讀取,減少異常情況下對(duì)業(yè)務(wù)的不利影響
業(yè)務(wù)邏輯通過KVO方式使用配置信息,保證客戶端配置更新時(shí),業(yè)務(wù)邏輯及時(shí)響應(yīng)
核心代碼如下:
- (void)readCommonConfigWithKey:(NSString*)keyString
configType:(configType)type
Finish:(finishReadConfigInfo)finish {
if(isEmptyString(keyString)) {
finish(nil);
return;
}
DDAppConfigModel*configModel = nil;
if(type==configTypeCommon) {
configModel = [self.commonConfigDictInfo objectForKey:keyString];
if(configModel == nil) {
[self updateConfigWithKey:keyStringtype:typefinish:finish];
}else{
dispatch_async(dispatch_get_main_queue(), ^{
finish(configModel);
});
}
}elseif(type==configTypeAppoint) {
[self updateConfigWithKey:keyStringtype:typefinish:finish];
}
}
代碼主要邏輯如下:
業(yè)務(wù)邏輯請(qǐng)求配置時(shí),優(yōu)先從內(nèi)存讀取
內(nèi)存沒有,從本地?cái)?shù)據(jù)存儲(chǔ)中查找
本地沒有,向服務(wù)端拉取配置信息
最后,通過block方式通知調(diào)用方,并返回結(jié)果
小結(jié)
根據(jù)上面的設(shè)計(jì),達(dá)達(dá)APP配置系統(tǒng)已經(jīng)實(shí)現(xiàn)了如下功能:
可以針對(duì)所有用戶、某個(gè)城市的用戶、某個(gè)平臺(tái)(iOS,Android)的用戶、某個(gè)APP(達(dá)達(dá),商家,派樂趣)的用戶以及某些指定id的用戶配置參數(shù),來實(shí)現(xiàn)不同用戶不同客戶端的業(yè)務(wù)邏輯定制
配置生效后,可以給配置用戶發(fā)送推送,讓APP主動(dòng)獲取配置的變更,并實(shí)時(shí)對(duì)APP的邏輯進(jìn)行調(diào)整
APP每次去服務(wù)器拉取配置的時(shí)候,服務(wù)器會(huì)根據(jù)客戶端已有的配置信息返回配置變更的增量,以便節(jié)省流量。
更多關(guān)于達(dá)達(dá)技術(shù)的文章,敬請(qǐng)關(guān)注達(dá)達(dá)技術(shù)公眾號(hào)。