秒杀系统(二)——商品模块展示技术难点
秒殺系統(tǒng)——商品模塊展示技術(shù)難點(diǎn)
商品詳情頁
商品詳情頁是展示商品詳細(xì)信息的一個(gè)頁面,承載在網(wǎng)站的大部分流量和訂單的入口。京東商城目前有通用版、全球購、閃購、易車、惠買車、服裝、拼購、今日抄底等許多套模板。各套模板的元數(shù)據(jù)是一樣的,只是展示方式不一樣。目前商品詳情頁個(gè)性化需求非常多,數(shù)據(jù)來源也是非常多的,而且許多基礎(chǔ)服務(wù)做不了的都放我們這,因此我們需要一種架構(gòu)能快速響應(yīng)和優(yōu)雅的解決這些需求問題。
因此我們重新設(shè)計(jì)了商品詳情頁的架構(gòu),主要包括三部分:
-
商品詳情頁系統(tǒng):商品詳情頁系統(tǒng)負(fù)責(zé)靜的部分
-
商品詳情頁統(tǒng)一服務(wù)系統(tǒng):統(tǒng)一服務(wù)負(fù)責(zé)動(dòng)的部分
-
商品詳情頁動(dòng)態(tài)服務(wù)系統(tǒng):動(dòng)態(tài)服務(wù)負(fù)責(zé)給內(nèi)網(wǎng)其他系統(tǒng)提供一些數(shù)據(jù)服務(wù)
商品詳情頁前端結(jié)構(gòu)
前端展示可以分為這么幾個(gè)維度:商品維度(標(biāo)題、圖片、屬性等)、主商品維度(商品介紹、規(guī)格參數(shù))、分類維度、商家維度、店鋪維度等;另外還有一些實(shí)時(shí)性要求比較高的如實(shí)時(shí)價(jià)格、實(shí)時(shí)促銷、廣告詞、配送至、預(yù)售等是通過異步加載。
SPU:?Standard Product Unit (標(biāo)準(zhǔn)化產(chǎn)品單元),SPU是商品信息聚合的最小單位,是一組可復(fù)用、易檢索的標(biāo)準(zhǔn)化信息的集合,該集合描述了一個(gè)產(chǎn)品的特性。
SKU: Stock keeping unit(庫存量單位) SKU即庫存進(jìn)出計(jì)量的單位(買家購買、商家進(jìn)貨、供應(yīng)商備貨、工廠生產(chǎn)都是依據(jù)SKU進(jìn)行的),在服裝、鞋類商品中使用最多最普遍。 例如紡織品中一個(gè)SKU通常表示:規(guī)格、顏色、款式。SKU是物理上不可分割的最小存貨單元。
單品頁流量特點(diǎn)
熱點(diǎn)少,各種爬蟲、比價(jià)軟件抓取。
2.1、壓測測試,進(jìn)行壓力測試
提升系統(tǒng)反應(yīng)速度方法:
1、換數(shù)據(jù)庫 ——換數(shù)據(jù)庫
2、分庫分表——進(jìn)行優(yōu)化
下圖是我對(duì)電商商品進(jìn)行Jmeter壓測的截圖。
Jmeter上圖主要看兩個(gè)參數(shù)Average和Throuhtput
其中平均值越小越好,吞吐量是越大越好。
其中遇到情況,就是有時(shí)候請(qǐng)求數(shù)量過大超過系統(tǒng)承受力,吞吐量更大,是后面大量請(qǐng)求錯(cuò)誤,進(jìn)行壓測的時(shí)候需要注意。
2.2、后臺(tái)
影響系統(tǒng)主要的開銷是兩方面 ——磁盤IO 、網(wǎng)絡(luò)IO
獲取商品詳情信息,下面是我獲取商品詳細(xì)頁的部分代碼
/*** 獲取商品詳情信息** @param id 產(chǎn)品ID*/ public PmsProductParam getProductInfo(Long id) {PmsProductParam productInfo = portalProductDao.getProductInfo(id);if (null == productInfo) {return null;}FlashPromotionParam promotion = flashPromotionProductDao.getFlashPromotion(id);if (!ObjectUtils.isEmpty(promotion)) {productInfo.setFlashPromotionCount(promotion.getRelation().get(0).getFlashPromotionCount());productInfo.setFlashPromotionLimit(promotion.getRelation().get(0).getFlashPromotionLimit());productInfo.setFlashPromotionPrice(promotion.getRelation().get(0).getFlashPromotionPrice());productInfo.setFlashPromotionRelationId(promotion.getRelation().get(0).getId());productInfo.setFlashPromotionEndDate(promotion.getEndDate());productInfo.setFlashPromotionStartDate(promotion.getStartDate());productInfo.setFlashPromotionStatus(promotion.getStatus());}return productInfo; }壓測結(jié)果:
采用5000并發(fā),可以看到異常率很高,下面進(jìn)行優(yōu)化。
靜態(tài)化處理
將網(wǎng)頁頁面進(jìn)行靜態(tài)化處理,把它放在CDN(Content Delivery Network內(nèi)容轉(zhuǎn)發(fā)器)上。
不直接訪問數(shù)據(jù)庫,轉(zhuǎn)而去訪問CDN。采用FreeMarker工具生成靜態(tài)化工具。
FreeMarker 是一款模板引擎:即基于模板和數(shù)據(jù)源生成輸出文本(html網(wǎng)頁,配置文件,電子郵件,源代碼)的通用工具。它是一個(gè) java 類庫,最初被設(shè)計(jì)用來在MVC模式的Web開發(fā)框架中生成HTML頁面,它沒有被綁定到Servlet或HTML或任意Web相關(guān)的東西上。也可以用于非Web應(yīng)用環(huán)境中。
模板編寫使用FreeMarker Template Language(FTL)。使用方式類似JSP的EL表達(dá)式。模板中專注于如何展示數(shù)據(jù),模板之外可以專注于要展示什么數(shù)據(jù)。
使用模板Template和數(shù)據(jù)源 Java Object生成輸出文本(html網(wǎng)頁、配置文件、電子郵件、源代碼)
pom引入:
<dependency><groupId>org.freemarker</groupId><artifactId>freemarker</artifactId><version>2.3.23</version></dependency>來一個(gè)demo:
使用步驟:
第一步:創(chuàng)建一個(gè)Configuration對(duì)象,直接new一個(gè)對(duì)象。構(gòu)造方法的參數(shù)就是freemarker對(duì)于的版本號(hào)。
第二步:設(shè)置模板文件所在的路徑。
第三步:設(shè)置模板文件使用的字符集。一般就是utf-8.
第四步:加載一個(gè)模板,創(chuàng)建一個(gè)模板對(duì)象。
第五步:創(chuàng)建一個(gè)模板使用的數(shù)據(jù)集,可以是pojo也可以是map。一般是Map。
第六步:創(chuàng)建一個(gè)Writer對(duì)象,一般創(chuàng)建一FileWriter對(duì)象,指定生成的文件名。
第七步:調(diào)用模板對(duì)象的process方法輸出文件。
第八步:關(guān)閉流。
public class FreeMarkTest {public static void main(String[] args) throws Exception {// 第一步:創(chuàng)建一個(gè)Configuration對(duì)象,直接new一個(gè)對(duì)象。構(gòu)造方法的參數(shù)就是freemarker對(duì)于的版本號(hào)。Configuration configuration = new Configuration(Configuration.getVersion());// 第二步:設(shè)置模板文件所在的路徑。configuration.setDirectoryForTemplateLoading(new File("D:\\ProgramData\\ftl"));// 第三步:設(shè)置模板文件使用的字符集。一般就是utf-8.configuration.setDefaultEncoding("utf-8");// 第四步:加載一個(gè)模板,創(chuàng)建一個(gè)模板對(duì)象。Template template = configuration.getTemplate("test.ftl");// 第五步:創(chuàng)建一個(gè)模板使用的數(shù)據(jù)集,可以是pojo也可以是map。一般是Map。Map dataModel = new HashMap<>();//向數(shù)據(jù)集中添加數(shù)據(jù)dataModel.put("hello", "我們來測試下數(shù)據(jù)看可以顯示出來嘛");// 第六步:創(chuàng)建一個(gè)Writer對(duì)象,一般創(chuàng)建一FileWriter對(duì)象,指定生成的文件名。Writer out = new FileWriter(new File("D:\\ProgramData\\ftl\\test.html"));// 第七步:調(diào)用模板對(duì)象的process方法輸出文件。template.process(dataModel, out);// 第八步:關(guān)閉流。out.close();} <h1> ${hello} </h1>list標(biāo)簽:
<#list studentList as student> ${student.id}/${studnet.name} </#list>if條件標(biāo)簽:
<#if student_index % 2 == 0> <#else> </#if>Null值的處理:
<#if a??> a不為空時(shí)。。 <#else> a為空時(shí)### </#if>日期標(biāo)簽:
當(dāng)前日期: ${date?date}當(dāng)前時(shí)間:${date?time}當(dāng)前日期和時(shí)間:${date?datetime}自定義日期格式:${date?string("yyyyMM/dd HH:mm: ss")}包含標(biāo)簽:
<#include "hello.ftl"/>實(shí)戰(zhàn):
ItemController
@RestController@Api(description = "商品列表信息")@RequestMapping("/item")public class ItemController {@AutowiredItemService itemService;@RequestMapping(value = "/static/{id}",method = RequestMethod.GET)@ApiOperation(value = "靜態(tài)化商品")public CommonResult<String> buildStatic(@PathVariable Long id){String path = itemService.toStatic(id);if(StringUtils.isEmpty(path)){return CommonResult.failed("靜態(tài)化商品頁面出現(xiàn)異常");}return CommonResult.success(path);}}接口:
public interface ItemService {/*** 靜態(tài)化商品詳情頁* @param id* @return*/String toStatic(Long id);}靜態(tài)化核心代碼: ItemServiceImpl
@Overridepublic String toStatic(Long id) {//查詢商品信息PmsProduct pmsProduct=productMapper.selectByPrimaryKey(id);if (pmsProduct==null){return null;}String outPath="";try {String userHome = System.getProperty("user.home");// 第一步:創(chuàng)建一個(gè)Configuration對(duì)象,直接new一個(gè)對(duì)象。構(gòu)造方法的參數(shù)就是freemarker對(duì)于的版本號(hào)。Configuration configuration = new Configuration(Configuration.getVersion());// 第二步:設(shè)置模板文件所在的路徑。configuration.setDirectoryForTemplateLoading(new File(userHome+"/template/ftl"));// 第三步:設(shè)置模板文件使用的字符集。一般就是utf-8.configuration.setDefaultEncoding("utf-8");// 第四步:加載一個(gè)模板,創(chuàng)建一個(gè)模板對(duì)象。Template template = null;template = configuration.getTemplate("report.ftl");// 第五步:創(chuàng)建一個(gè)模板使用的數(shù)據(jù)集,可以是pojo也可以是map。一般是Map。Map dataModel = new HashMap();// 向數(shù)據(jù)集中添加數(shù)據(jù)dataModel.put("item", pmsProduct);String images= pmsProduct.getPic();if(StringUtils.isNotEmpty(images)){String[] split = images.split(",");List<String> imageList= Arrays.asList(split);dataModel.put("imageList", imageList);}// 第六步:創(chuàng)建一個(gè)Writer對(duì)象,一般創(chuàng)建一FileWriter對(duì)象,指定生成的文件名。outPath=userHome+"/template/report/1000"+pmsProduct.getId()+".html";Writer out = new FileWriter(new File(outPath));// 第七步:調(diào)用模板對(duì)象的process方法輸出文件。template.process(dataModel, out);// 第八步:關(guān)閉流。out.close();} catch (IOException e) {e.printStackTrace();} catch (TemplateException te) {te.printStackTrace();}return outPath;}前端:pms/index.vue
<el-buttonsize="mini"@click="product_static(scope.$index, scope.row)">靜</el-button> 定義vue的product_static方法的js代碼script:
product_static(index,obj){ console.log(index,obj.id)this.$confirm('確認(rèn)要靜態(tài)化', '提示', {confirmButtonText: '確定',cancelButtonText: '取消',type: 'warning'}).then(()=>{productStatic(obj.id).then(response=>{this.$message({message: '靜態(tài)化成功',type: 'success',duration: 1000});this.editSkuInfo.dialogVisible=false;});});}product.js:
export function productStatic(id) {return request({url:'/item/static/'+id,method:'get',})}優(yōu)化:如果發(fā)生價(jià)格改變、秒殺 or 倒計(jì)時(shí)、下單的情況下 ?? 由于靜態(tài)化文件不能實(shí)時(shí)修改 ,js沒有生效(js、css、圖片url)
這個(gè)方案只適合小流量架構(gòu) 。為什么?
re:如果類似京東這種商品級(jí)別,使用頁面靜態(tài)化,每次修改頁面欄位需要生成的新頁面太多,并不適合。
小流量架構(gòu):https://www.processon.com/view/link/5e5774dae4b0cb56daac5a80
分布式場景下:
1000個(gè)靜態(tài)商品頁面使用 1個(gè)模板,當(dāng)商品界面發(fā)生修改,需要修改的頁面數(shù)量:1000個(gè)靜態(tài)商品頁面 * 機(jī)房(服務(wù))數(shù)量;
公式 :1000個(gè)靜態(tài)頁面*機(jī)房數(shù)量。
過程: 是修改了一個(gè)字段,然后生成1k個(gè)靜態(tài)頁面,然后拷貝到其他N-1臺(tái)服務(wù)器上
小米:1000個(gè)商品頁面 12臺(tái) 12000個(gè)靜態(tài)化數(shù)據(jù) CDN 12*1000 個(gè)靜態(tài)化頁面
京東:10000000個(gè)商品也米娜 50臺(tái) 上億級(jí)別靜態(tài)化頁面, 京東商品多,靜態(tài)化頁面太多
插入、修改、數(shù)據(jù)調(diào)整,這些都需要重新生成靜態(tài)頁面。
1個(gè)模板改了所有的靜態(tài)化頁面跟著改, 如果修改靜態(tài)頁面的一個(gè)字段(如果改個(gè)字段),需要重新生成所有的靜態(tài)頁面
架構(gòu)方案的問題:
問題一:
我們知道數(shù)據(jù)新增分:增量和全量數(shù)據(jù)
如果后臺(tái)的小二新增了很多的商品,那我們都要對(duì)這些商品進(jìn)行靜態(tài)化,但是現(xiàn)在有個(gè)問題。那這些數(shù)據(jù)如何同步了?這是一個(gè)新增商品同步的問題,那這個(gè)問題怎么解決比較好了?。
背景:不同應(yīng)用部署在不同服務(wù)器甚至在不同的機(jī)房不同的國家。數(shù)據(jù)修改后,需要進(jìn)行數(shù)據(jù)同步
同步的方案
1、通過網(wǎng)絡(luò)同步的方式 就是其中一臺(tái)服務(wù)器靜態(tài)化之后,然后把文件同步到其他應(yīng)用服務(wù)器上去。比如我們的linux命令scp方式。這種方式雖然可行,但是我們發(fā)現(xiàn)問題還是蠻多的,有多少個(gè)節(jié)點(diǎn)就需要同步多少份,等于是商品的數(shù)量*服務(wù)器的應(yīng)用數(shù)數(shù)。很顯然這種辦法不是最優(yōu)的解決辦法
如果上述辦法無法解決,那我們就用另外的方案,同學(xué)們你們覺得還有其他的方案沒有?
**2、定時(shí)任務(wù):**可以在某個(gè)應(yīng)用用一個(gè)定時(shí)任務(wù),然后分別去執(zhí)行數(shù)據(jù)庫需要靜態(tài)化的數(shù)據(jù)即可,可以解決上述1數(shù)據(jù)同步的問題,因?yàn)樗械娜蝿?wù)都是在本機(jī)運(yùn)行,就不需要數(shù)據(jù)同步了。但是也有一個(gè)問題。就是如何避免不通的機(jī)器跑的數(shù)據(jù)不要重復(fù),也就是A和B定時(shí)任務(wù)都跑了一份商品。這個(gè)是這種方案需要解決的。(比較直觀的就是上鎖) 我理解:就是每個(gè)節(jié)點(diǎn)服務(wù)器上啟動(dòng)定時(shí)任務(wù),自動(dòng)去復(fù)制靜態(tài)化頁面和數(shù)據(jù)
**3、消息中間件:**還有一種辦法就是通過消息中間件來解決。訂閱topic然后生成當(dāng)前服務(wù)器靜態(tài)化的頁面。
問題二:
我們的freemark它是數(shù)據(jù)要事先按我這個(gè)模板生產(chǎn)好的,那就是說一定你改了模板,如果要生效的話,需要重新在把數(shù)據(jù)取出來和我們這個(gè)模板進(jìn)行匹配生產(chǎn)更多的的靜態(tài)html文件。那這是一個(gè)比較大的問題
如果后臺(tái)數(shù)據(jù)有變更呢?如何及時(shí)同步到其它服務(wù)端?
如果頁面靜態(tài)化了,我們搜索打開一個(gè)商品詳細(xì)頁,怎么知道要我需要的訪問的靜態(tài)頁面?
萬一我們模板需要修改了怎么辦?
牽一發(fā)動(dòng)全身。
后臺(tái)優(yōu)化:
redis緩存:
redis設(shè)置:RedisConifg》RedisOpsUtil
/*** 獲取商品詳情信息** @param id 產(chǎn)品ID*/ public PmsProductParam getProductInfo(Long id) {PmsProductParam productInfo = null;//從緩存Redis里找productInfo = redisOpsUtil.get(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE + id, PmsProductParam.class);if(null!=productInfo){return productInfo;}productInfo = portalProductDao.getProductInfo(id);if (null==productInfo) {log.warn("沒有查詢到商品信息,id:"+id);return null;}FlashPromotionParam promotion = flashPromotionProductDao.getFlashPromotion(id);if (!ObjectUtils.isEmpty(promotion)) {productInfo.setFlashPromotionCount(promotion.getRelation().get(0).getFlashPromotionCount());productInfo.setFlashPromotionLimit(promotion.getRelation().get(0).getFlashPromotionLimit());productInfo.setFlashPromotionPrice(promotion.getRelation().get(0).getFlashPromotionPrice());productInfo.setFlashPromotionRelationId(promotion.getRelation().get(0).getId());productInfo.setFlashPromotionEndDate(promotion.getEndDate());productInfo.setFlashPromotionStartDate(promotion.getStartDate());productInfo.setFlashPromotionStatus(promotion.getStatus());}redisOpsUtil.set(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE + id, productInfo, 3600, TimeUnit.SECONDS);return productInfo; }好處:
加入redis之后我們發(fā)現(xiàn)提高了可以把之前請(qǐng)求 數(shù)據(jù)庫查詢的商品都緩存到redis中,通過對(duì)redis的訪問來減少對(duì)數(shù)據(jù)里的依賴,減少了依賴本質(zhì)就是減少了磁盤IO。
問題:
提高請(qǐng)求的吞吐量,除了減少磁盤IO,還有網(wǎng)絡(luò)IO,我們可以發(fā)現(xiàn),請(qǐng)求redis其實(shí)也會(huì)涉及到網(wǎng)絡(luò)IO,我們所有的請(qǐng)求都要走xxx端口號(hào)。這個(gè)問題下一篇再總結(jié)
壓力測試:
我們發(fā)現(xiàn)吞吐量有一定的提高。但是問題還是有的。
總結(jié)
以上是生活随笔為你收集整理的秒杀系统(二)——商品模块展示技术难点的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 微信小程序模板消息限制,实现无限制主动推
- 下一篇: 酷我音乐PC客户端歌词解密 - node