第二屆中國國際智慧網聯汽車論壇2017 – 智慧汽車網聯化資訊安全問題不容忽視!

2017年11月16-17日∣中國·上海

路協同發展創造全面感知新時代

 

隨著電子、資訊、通信、人工智慧等技術與汽車產業加速融合,汽車產品正加快向智慧化、網聯化方向發展。因此,智慧網聯汽車面臨的資訊安全挑戰也備受業界關注。

頂層設計政策體系為智慧網聯汽車的發展創建了良好的發展環境,與此相關的大資料、雲計算、人工智慧等也在持續提供著技術保障。與此同時,一個較為顯著的問題是,汽車的網聯化也極有可能徹底打開了駭客入侵智慧網聯汽車的通道。智慧網聯汽車與外部的每個介面都可能被惡意利用,每個控制單元都可能被駭客攻擊、病毒感染,智慧網聯汽車的資訊安全防護難度也因之而倍增。

第二屆中國國際智慧網聯汽車論壇將針對智慧網聯汽車資訊安全問題定向邀請包括騰訊科恩實驗室360奇虎梆梆安全中國移動中國聯通等行業內權威人士對於車聯網資訊安全問題進行更深層次的解析。此次論壇將涉及3個論壇,參觀考察及晚宴,共將有300位行業人士一起,對智慧網聯汽車發展面臨的挑戰、機遇與對策各方面進行為期兩天更深層次並具有建設和戰略性的探討。

 

會議亮點

Ø  豐富的內容:3大論壇的深度解析

Ø  參會嘉賓:300+高度滿意的企業決策者,160+業內知名企業,40+國家和地區

Ø  演講嘉賓:30+世界新能源汽車行業知名發言嘉賓

Ø  會議形式:3個論壇,2天會議,1個晚宴

 

會議結構

論壇一:智慧網聯汽車發展趨勢分析及國內外項目解析和智慧交通發展

 

論壇二:車載通訊資訊技術及車聯網未來發展

²  迎合中國製造2025,促進智慧網聯汽車發展之路

²  智慧汽車、車聯網、車載資訊服務:點、線、網、面的格局與階段

²  智慧汽車技術創新革命

²  智慧交通/汽車發展不同階段的分析

²  國際智慧交通與智慧駕駛的銜接發展

 

²  車載半導體的機遇與挑戰

²  車聯網最新技術探討

²  4G通信在車載行業的應用

²  分時租賃-建造全民共用汽車

²  移動互聯網運營與智慧汽車的融合

論壇三:智慧汽車ADAS駕駛輔助系統和智慧駕駛技術

 

考察活動:20171115

²  ADAS與智慧駕駛解決方案探討

²  ADAS駕駛輔助系統性能及匹配測試

²  駕駛輔助系統雷達與感測器的核心技術

²  高精准地圖對於智慧駕駛的重要性

²  汽車人機交互對於智慧駕駛的重要性及發展展望

 

1.參觀上海天合汽車安全系統有限公司

2.參觀上海智慧網聯汽車試點示範區-中國首家(已預訂,如無測試企業屆時即可參觀)

 

若您對峰會有更多要求,請撥打021-6093 0815與我們聯繫,謝謝理解和支持!

我們期待與貴單位一起出席於20171116-17上海舉辦的第二屆中國國際智慧網聯汽車論壇2017,以利決策!

 

欲知更多會議詳情,請登陸官方網站:http://www.ourpolaris.com/2017/icv/index_c.html

連絡人:Latika LIU(劉小姐)

電話:021-6093 0815

傳真:021-6047 5887

郵箱:

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

中國工信部:將制定停止產銷傳統能源汽車的時間表

新華社9日報導,中國工信部副部長辛國斌表示,一些國家已經制定了停止生產銷售傳統能源汽車的時間表。他說,目前工信部也啟動了相關研究、將會同相關部門制定中國的時間表。

報導指出,從現在到2025年將是汽車產業變革最為劇烈的幾年,傳統汽車節能減排要求越來越高,新能源汽車發展加快的同時對技術要求也越來越高,智能聯網將對整個產業巨大影響。辛國斌及專家建議中國車企應深刻認識這種趨勢、及時調整策略。

Thomson Reuters上個月底引述消息人士報導,根據最新提案,明年底中國境內車商8%銷售必須是電動車或油電混合車種、2019年升至10%、2020年升至12%。報導指出,這項規定預計將自2019年起開始落實執行、較原先規劃晚一年。

英國跟隨法國以及馬德里、墨西哥城和雅典等城市的抗空汙腳步,7月宣布將自2040年起禁止販售汽油和柴油新車。英國最大汽車製造商Jaguar Land Rover(JLR)9月7日宣布,2020年起旗下所有新車都將具備電動或油電混合驅動選項。德國車廠BMW也宣布將自2020年起開始量產電動車、預估到2025年將有12種純電動車款。

BBC News 10日報導,上述最新消息將對中國石油需求帶來連鎖效應。中國目前是全球第二大石油消費國。依據目前的規劃,中國希望在2025年將電動車/油電混合車銷售佔比至少拉升至五分之一。

根據DNV GL首度發布的「能源轉型展望」報告,受電動車滲透率持續上揚的影響,石油供應將在2020-2028年期間轉趨持平、隨後大幅下降,2034年將遭天然氣超越。

這份報告預估電動車、內燃引擎車將在2022年達到「成本平價」,預估到2033年全球半數輕型新車銷售量都將是電動車。

(本文內容由授權使用。圖片出處:public domain CC0)

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

x86彙編分頁模式實驗 –《ORANGE’S一個操作系統的實現》中 pmtest8.asm解析

  序言(廢話) : 在看書的過程中發現一開始不是很能理解pmtest8的目的,以及書上說得很抽象..於是在自己閱讀過源代碼后,將一些自己的心得寫在這裏。

  正文 : 

  講解順序依然按照書上貼代碼的順序來。但是是幾乎逐句解釋的。可能會稍微有點啰嗦。廢話就不多說了直接貼代碼。

LABEL_DESC_FLAT_C:  Descriptor 0,        0fffffh, DA_CR|DA_32|DA_LIMIT_4K; 0~4G
LABEL_DESC_FLAT_RW: Descriptor 0,        0fffffh, DA_DRW|DA_LIMIT_4K     ; 0~4G
SelectorFlatC       equ    LABEL_DESC_FLAT_C - LABEL_GDT                
SelectorFlatRW        equ    LABEL_DESC_FLAT_RW - LABEL_GDT

  顯然,兩個分別是 FLAT_C 和  FLAT_RW 的描述符和選擇子。

  問題 : 為什麼要有這兩個東西?

  解釋 : FLAT_C是用來執行的非一致性32位代碼段,粒度為4k,也就是 limit(段限長) = (0xfffff + 1)  * 4k = 4G,FLAT_RW 是用來修改數據的,因為需要利用這個描述符的權限(可寫)來將代碼寫入到目的地(這個目的地允許在 0 – 4G區間內)。之所以要分兩個選擇符,是防止在執行的時候修改代碼(所以FLAT_C不能給寫的權限),但是又必須在執行之前進行複製,所以一定要有一個入口能提供寫入的方式,於是設置兩個描述符來進行。這樣既安全又有章法。

 

SetupPaging:
    ; 根據內存大小計算應初始化多少PDE以及多少頁表
    xor    edx, edx
    mov    eax, [dwMemSize]
    mov    ebx, 400000h    ; 400000h = 4M = 4096 * 1024, 一個頁表對應的內存大小
    div    ebx
    mov    ecx, eax    ; 此時 ecx 為頁表的個數,也即 PDE 應該的個數
    test    edx, edx
    jz    .no_remainder
    inc    ecx        ; 如果餘數不為 0 就需增加一個頁表
.no_remainder:
    mov    [PageTableNumber], ecx    ; 暫存頁表個數

    ; 為簡化處理, 所有線性地址對應相等的物理地址. 並且不考慮內存空洞.

    ; 首先初始化頁目錄
    mov    ax, SelectorFlatRW
    mov    es, ax
    mov    edi, PageDirBase0    ; 此段首地址為 PageDirBase0
    xor    eax, eax
    mov    eax, PageTblBase0 | PG_P  | PG_USU | PG_RWW
.1:    ; es:edi 初始等於 PageDirBase0 (當前頁目錄表項), eax 初始基地址等於 PageTblBase0
    stosd
    add    eax, 4096        ; 為了簡化, 所有頁表在內存中是連續的.
    loop    .1

    ; 再初始化所有頁表
    mov    eax, [PageTableNumber]    ; 頁表個數
    mov    ebx, 1024        ; 每個頁表 1024 個 PTE
    mul    ebx
    mov    ecx, eax        ; PTE個數 = 頁表個數 * 1024
    mov    edi, PageTblBase0    ; 此段首地址為 PageTblBase0
    xor    eax, eax
    mov    eax, PG_P  | PG_USU | PG_RWW
.2:    ; es:edi 初始等於 PageTblBase0 (當前頁表項), eax = 0 (線性地址 = 物理地址)
    stosd
    add    eax, 4096        ; 每一頁指向 4K 的空間
    loop    .2

    mov    eax, PageDirBase0
    mov    cr3, eax
    mov    eax, cr0
    or    eax, 80000000h
    mov    cr0, eax
    jmp    short .3
.3:
    nop

    ret

 

  這段代碼我加註了兩句註釋 分別在 .1 和 .2 這兩個標籤那行,其實這裏和之前的setPaging並沒有很大的區別,需要注意的就是 這裏的 頁目錄表 的地址是  PageDirBase0, 頁表的地址是PageTblBase0,強調這點的原因在於之後的  PSwitch 這個函數中則是 PageDirBase1 和 PageTblBase1。也就是說實際上數據中有兩個頁面管理的數據結構(頁目錄表和頁表合起來相當於一個管理頁面的數據結構)。

 1 PagingDemo:
 2     mov    ax, cs
 3     mov    ds, ax
 4     mov    ax, SelectorFlatRW        ; 設置es為基地址為0的可讀寫的段(便於複製代碼)
 5     mov    es, ax
 6     
 7     push    LenFoo
 8     push    OffsetFoo
 9     push    ProcFoo            ; 00401000h
10     call    MemCpy        
11     add    esp, 12
12 
13     push    LenBar            ; 被複制代碼段(但是以ds為段基址)的長度 
14     push    OffsetBar        ; 被複制代碼段(但是以ds為段基址)的段偏移量
15     push    ProcBar            ; 目的代碼段的物理空間地址 00501000h
16     call    MemCpy
17     add    esp, 12
18 
19     push    LenPagingDemoAll
20     push    OffsetPagingDemoProc    
21     push    ProcPagingDemo            ; [es:ProcPagingDemo] = ProcPagingDemo = 00301000h
22     call    MemCpy
23     add    esp, 12
24 
25     mov    ax, SelectorData
26     mov    ds, ax            ; 數據段選擇子
27     mov    es, ax
28 
29     call    SetupPaging        ; 啟動分頁
30     ; 當前線性地址依然等於物理地址
31     call    SelectorFlatC:ProcPagingDemo    ; 訪問的線性地址為 00301000h,物理地址也是 00301000h
32     call    PSwitch            ; 切換頁目錄,改變地址映射關係
33     call    SelectorFlatC:ProcPagingDemo    ; 訪問的線性地址為 00301000h
34 
35     ret

  在這裏首先要說明的是 MemCpy函數,這個函數有三個參數分別表示 : 

   1)被複制段(但是以ds為段基址)的 長度 
   2)被複制段(但是以ds為段基址)的 段偏移量
   3)目的地的物理空間地址(之所以說是物理空間是因為當前線性地址等於物理地址,以es為段基址,但是es的段基址為0)
功能則是 將被複制段 的數據複製 參數1)的長度字節 去目的地去(簡單說就是利用三個參數複製數據)

我們可以知道的是在上面代碼中三次調用 MemCpy 都沒有進入分頁模式,也就是說當下線性地址等於物理地址。那麼根據我上面的註釋就可以知道三個代碼分別複製到哪裡去了。
之後就是恢複數據段(之前將ds = cs,是為了複製代碼),然後啟動分頁(上面已經講了),然後啟動分頁后當前線性地址依然等於物理地址。
這個時候第一次調用 call SelectorFlatC:ProcPagingDemo,也就是訪問的線性地址為 00301000h,物理地址也是 00301000h的代碼(之前移動過去的)。
 下面這段代碼就是被移動到00301000h的代碼,這段代碼只做了一件事那就是調用 [cs:LinearAddrDemo]的代碼,但請注意,由於 call SelectorFlatC:ProcPagingDemo
所以此時的 cs = SelectorFlatC,也就是說段基址等於0,於是實際上這段代碼的功能就是訪問 物理地址為00401000h處的代碼。
PagingDemoProc:
OffsetPagingDemoProc    equ    PagingDemoProc - $$
    mov    eax, LinearAddrDemo
    call    eax        ; 未開始PSwitch前, eax = ProcFoo = 00401000h (cs 的段基址 = 0)
    retf
LenPagingDemoAll    equ    $ - PagingDemoProc

  而物理地址00401000h處就是ProcFoo的代碼(第一次調用MemCpy拷貝的代碼)。被拷貝的代碼如下

foo:
OffsetFoo        equ    foo - $$
    mov    ah, 0Ch            ; 0000: 黑底    1100: 紅字
    mov    al, 'F'
    mov    [gs:((80 * 17 + 0) * 2)], ax    ; 屏幕第 17 行, 第 0 列。
    mov    al, 'o'
    mov    [gs:((80 * 17 + 1) * 2)], ax    ; 屏幕第 17 行, 第 1 列。
    mov    [gs:((80 * 17 + 2) * 2)], ax    ; 屏幕第 17 行, 第 2 列。
    ret
LenFoo            equ    $ - foo

  功能很明顯就是現實一個字符串 Foo而已。

總結第一次分頁后的動作:

  就是拷貝三份代碼分別到ProcFoo, ProcBar, ProcPagingDemo 處(這四個都是物理內存哦,並且後面因為段基址是0(FLAT_C 段基址)於是很容易地就訪問到了物理地址)。然後開啟分頁模式(其實幾乎沒什麼影響 因為仍然和分段一樣 線性地址 = 物理地址)。然後調用 被拷貝的函數 ProcPagingDemo ,ProcPagingDemo 函數調用 ProcFoo函數,显示字符 “Foo”然後兩次返回。

第二次分頁 : call PSwitch

被調用代碼如下 :

 1 PSwitch:
 2     ; 初始化頁目錄
 3     mov    ax, SelectorFlatRW
 4     mov    es, ax
 5     mov    edi, PageDirBase1    ; 此段首地址為 PageDirBase1
 6     xor    eax, eax
 7     mov    eax, PageTblBase1 | PG_P  | PG_USU | PG_RWW
 8     mov    ecx, [PageTableNumber]
 9 .1:    ; es:edi 初始等於 PageDirBase1 (當前頁目錄表項), eax 初始基地址等於 PageTblBase1
10     stosd
11     add    eax, 4096        ; 為了簡化, 所有頁表在內存中是連續的.
12     loop    .1
13 
14     ; 再初始化所有頁表
15     mov    eax, [PageTableNumber]    ; 頁表個數
16     mov    ebx, 1024        ; 每個頁表 1024 個 PTE
17     mul    ebx
18     mov    ecx, eax        ; PTE個數 = 頁表個數 * 1024
19     mov    edi, PageTblBase1    ; 此段首地址為 PageTblBase1
20     xor    eax, eax
21     mov    eax, PG_P  | PG_USU | PG_RWW
22 .2: ; es:edi 初始等於 PageTblBase1 (當前頁表項), eax 初始基地址等於 0(線性地址等於物理地址)
23     stosd
24     add    eax, 4096        ; 每一頁指向 4K 的空間
25     loop    .2
26 
27     ; 在此假設內存是大於 8M 的
28     ; 下列代碼將LinearAddrDemo所處的頁表的相對第一個頁表的偏移地址放入ecx中
29     mov    eax, LinearAddrDemo
30     shr    eax, 22
31     mov    ebx, 4096        ; (LinearAddrDemo / 4M)表示第幾個頁表
32     mul    ebx                ; 第幾個頁表 * 4k (1024(一個頁表項的數量) * 4(一個頁表項的字節))
33     mov    ecx, eax        ; 也就是對應頁表的偏移地址
34     
35     ; 下列代碼將LinearAddrDemo所處的頁表項相對第一個頁表項的偏移地址放入eax中
36     mov    eax, LinearAddrDemo
37     shr    eax, 12            ; LinearAddrDemo / 4k,表示第幾個頁表項
38     and    eax, 03FFh    ; 1111111111b (10 bits)    ; 取低10位,也就是餘下的零散頁表項(一個頁表有2^10個頁表項)
39     mov    ebx, 4                                
40     mul    ebx                                    ; * 4 表示的是具體偏移字節數
41     add    eax, ecx                            ; eax = (((LinearAddrDemo / 2^12) & 03FFh) * 4) + (4k * (LinearAddrDemo / 2^22))
42     
43     
44     add    eax, PageTblBase1                    ; 第一個頁表的第一個頁表項
45     mov    dword [es:eax], ProcBar | PG_P | PG_USU | PG_RWW
46 
47     mov    eax, PageDirBase1
48     mov    cr3, eax
49     jmp    short .3
50 .3:
51     nop
52 
53     ret

  在這裏我加了幾個比較重要的註釋分別在第 9, 22, 28,35處。

  這段代碼做了什麼?

  首先是設置頁面管理的數據結構(頁表和頁目錄表),但是需要注意的是,這裏設置頁表和頁目錄表除了不是之前的頁面管理結構之外,其實內容是差不多的,也就是說當前(第25行)這裏的狀態也是 線性地址 = 物理地址 !!!

 但是在第27行做了一個操作,就是將LinearAddrDemo對應的 頁表項的地址 換成了 ProcBar(00501000h) 的地址。(具體如何實現的請看27-45行我寫的註釋)。
  在做完這些之後就返回第二次執行 call SelectorFlatC:ProcPagingDemo 了,在這個時候 cs = SelectorFlatC (段基址等於0), eip = ProcPagingDemo = 00301000h,也就是說訪問了
線性地址 = 00301000h處,但是這裏已經被修改,除了這個頁面之外,其他頁面都是 線性地址 = 物理地址,但是這裏 線性地址 = 00301000h ,映射的物理地址是 ProcBar(00501000h)
於是便調用了 ProcBar 段的代碼,而這段的代碼是第二次調用MemCpy時候複製過去的。被複制的具體代碼是:
bar:
OffsetBar        equ    bar - $$
    mov    ah, 0Ch            ; 0000: 黑底    1100: 紅字
    mov    al, 'B'
    mov    [gs:((80 * 18 + 0) * 2)], ax    ; 屏幕第 18 行, 第 0 列。
    mov    al, 'a'
    mov    [gs:((80 * 18 + 1) * 2)], ax    ; 屏幕第 18 行, 第 1 列。
    mov    al, 'r'
    mov    [gs:((80 * 18 + 2) * 2)], ax    ; 屏幕第 18 行, 第 2 列。
    ret
LenBar            equ    $ - bar
也就是显示一個字符串 "Bar", 然後返回到PagingDemo的最後一句 ret,再次返回。於是這段代碼也就結束了。
第二次代碼是如何實現調用 ProcBar的?
  通過將線性地址 = ProcPaging(00301000h)對應的頁表項的地址值給修改成了 PaocBar(00501000h)的物理地址,於是從 00301000h 的線性地址 映射到 00501000h的物理地址上去了,
但是其實其他地方(除了這個頁之外)的線性地址 = 物理地址依然成立。也是上面這段代碼很小,一定是小於 4k(一頁的大小),於是只需要修改一個頁表項就可以了!
 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※帶您來看台北網站建置台北網頁設計,各種案例分享

three.js使用gpu選取物體並計算交點位置

光線投射法

使用three.js自帶的光線投射器(Raycaster)選取物體非常簡單,代碼如下所示:

var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();

function onMouseMove(event) {
    // 計算鼠標所在位置的設備坐標
    // 三個坐標分量都是-1到1
    mouse.x = event.clientX / window.innerWidth * 2 - 1;
    mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
}

function pick() {
    // 使用相機和鼠標位置更新選取光線
    raycaster.setFromCamera(mouse, camera);

    // 計算與選取光線相交的物體
    var intersects = raycaster.intersectObjects(scene.children);
}

它是採用包圍盒過濾,計算投射光線與每個三角面元是否相交實現的。

但是,當模型非常大,比如說有40萬個面,通過遍歷的方法選取物體和計算碰撞點位置將非常慢,用戶體驗不好。

但是使用gpu選取物體不存在這個問題。無論場景和模型有多大,都可以在一幀內獲取到鼠標所在點的物體和交點的位置。

使用GPU選取物體

實現方法很簡單:

1.  創建選取材質,將場景中的每個模型的材質替換成不同的顏色。

2. 讀取鼠標位置像素顏色,根據顏色判斷鼠標位置的物體。

具體實現代碼:

1. 創建選取材質,遍歷場景,將場景中每個模型替換為不同的顏色。

let maxHexColor = 1;

// 更換選取材質
scene.traverseVisible(n => {
    if (!(n instanceof THREE.Mesh)) {
        return;
    }
    n.oldMaterial = n.material;
    if (n.pickMaterial) { // 已經創建過選取材質了
        n.material = n.pickMaterial;
        return;
    }
    let material = new THREE.ShaderMaterial({
        vertexShader: PickVertexShader,
        fragmentShader: PickFragmentShader,
        uniforms: {
            pickColor: {
                value: new THREE.Color(maxHexColor)
            }
        }
    });
    n.pickColor = maxHexColor;
    maxHexColor++;
    n.material = n.pickMaterial = material;
});

 

PickVertexShader:

void main() {
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

 

PickFragmentShader:

uniform vec3 pickColor;

void main() {
    gl_FragColor = vec4(pickColor, 1.0);
}

 

2.  將場景繪製在WebGLRenderTarget上,讀取鼠標所在位置的顏色,判斷選取的物體。

let renderTarget = new THREE.WebGLRenderTarget(width, height);
let pixel = new Uint8Array(4);

// 繪製並讀取像素
renderer.setRenderTarget(renderTarget);
renderer.clear();
renderer.render(scene, camera);
renderer.readRenderTargetPixels(renderTarget, offsetX, height - offsetY, 1, 1, pixel); // 讀取鼠標所在位置顏色

// 還原原來材質,並獲取選中物體
const currentColor = pixel[0] * 0xffff + pixel[1] * 0xff + pixel[2];

let selected = null;

scene.traverseVisible(n => {
    if (!(n instanceof THREE.Mesh)) {
        return;
    }
    if (n.pickMaterial && n.pickColor === currentColor) { // 顏色相同
        selected = n; // 鼠標所在位置的物體
    }
    if (n.oldMaterial) {
        n.material = n.oldMaterial;
        delete n.oldMaterial;
    }
});

說明:offsetX和offsetY是鼠標位置,height是畫布高度。readRenderTargetPixels一行的含義是選取鼠標所在位置(offsetX, height – offsetY),寬度為1,高度為1的像素的顏色。

pixel是Uint8Array(4),分別保存rgba顏色的四個通道,每個通道取值範圍是0~255。

完整實現代碼:

使用GPU獲取交點位置

實現方法也很簡單:

1. 創建深度着色器材質,將場景深度渲染到WebGLRenderTarget上。

2. 計算鼠標所在位置的深度,根據鼠標位置和深度計算交點位置。

具體實現代碼:

1. 創建深度着色器材質,將深度信息以一定的方式編碼,渲染到WebGLRenderTarget上。

深度材質:

const depthMaterial = new THREE.ShaderMaterial({
    vertexShader: DepthVertexShader,
    fragmentShader: DepthFragmentShader,
    uniforms: {
        far: {
            value: camera.far
        }
    }
});

DepthVertexShader:

precision highp float;

uniform float far;

varying float depth;

void main() {
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    depth = gl_Position.z / far;
}

DepthFragmentShader:

precision highp float;

varying float depth;

void main() {
    float hex = abs(depth) * 16777215.0; // 0xffffff

    float r = floor(hex / 65535.0);
    float g = floor((hex - r * 65535.0) / 255.0);
    float b = floor(hex - r * 65535.0 - g * 255.0);
    float a = sign(depth) >= 0.0 ? 1.0 : 0.0; // depth大於等於0,為1.0;小於0,為0.0。

    gl_FragColor = vec4(r / 255.0, g / 255.0, b / 255.0, a);
}

重要說明:

a. gl_Position.z是相機空間中的深度,是線性的,範圍從cameraNear到cameraFar。可以直接使用着色器varying變量進行插值。

b. gl_Position.z / far的原因是,將值轉換到0~1範圍內,便於作為顏色輸出。

c. 不能使用屏幕空間中的深度,透視投影后,深度變為-1~1,大部分非常接近1(0.9多),不是線性的,幾乎不變,輸出的顏色幾乎不變,非常不準確。

d. 在片元着色器中獲取深度方法:相機空間深度為
gl_FragCoord.z,屏幕空間深度為
gl_FragCoord.z /  gl_FragCoord.w

e. 上述描述都是針對透視投影,正投影中gl_Position.w為1,使用相機空間和屏幕空間深度都是一樣的。

f. 為了盡可能準確輸出深度,採用rgb三個分量輸出深度。gl_Position.z/far範圍在0~1,乘以0xffffff,轉換為一個rgb顏色值,r分量1表示65535,g分量1表示255,b分量1表示1。

 

完整實現代碼:

 

2. 讀取鼠標所在位置的顏色,將讀取到的顏色值還原為相機空間深度值。

a. 將“加密”處理后的深度繪製在WebGLRenderTarget上。讀取顏色方法

let renderTarget = new THREE.WebGLRenderTarget(width, height);
let pixel = new Uint8Array(4);

scene.overrideMaterial = this.depthMaterial;

renderer.setRenderTarget(renderTarget);

renderer.clear();
renderer.render(scene, camera);
renderer.readRenderTargetPixels(renderTarget, offsetX, height - offsetY, 1, 1, pixel);

說明:offsetX和offsetY是鼠標位置,height是畫布高度。readRenderTargetPixels一行的含義是選取鼠標所在位置(offsetX, height – offsetY),寬度為1,高度為1的像素的顏色。

pixel是Uint8Array(4),分別保存rgba顏色的四個通道,每個通道取值範圍是0~255。

 

b. 將“加密”后的相機空間深度值“解密”,得到正確的相機空間深度值。

if (pixel[2] !== 0 || pixel[1] !== 0 || pixel[0] !== 0) {
    let hex = (this.pixel[0] * 65535 + this.pixel[1] * 255 + this.pixel[2]) / 0xffffff;

    if (this.pixel[3] === 0) {
        hex = -hex;
    }

    cameraDepth = -hex * camera.far; // 相機坐標系中鼠標所在點的深度(注意:相機坐標系中的深度值為負值)
}

 

3. 根據鼠標在屏幕上的位置和相機空間深度,插值反算交點世界坐標系中的坐標。

let nearPosition = new THREE.Vector3(); // 鼠標屏幕位置在near處的相機坐標系中的坐標
let farPosition = new THREE.Vector3(); // 鼠標屏幕位置在far處的相機坐標系中的坐標
let world = new THREE.Vector3(); // 通過插值計算世界坐標

// 設備坐標
const deviceX = this.offsetX / width * 2 - 1;
const deviceY = - this.offsetY / height * 2 + 1;

// 近點
nearPosition.set(deviceX, deviceY, 1); // 屏幕坐標系:(0, 0, 1)
nearPosition.applyMatrix4(camera.projectionMatrixInverse); // 相機坐標系:(0, 0, -far)

// 遠點
farPosition.set(deviceX, deviceY, -1); // 屏幕坐標系:(0, 0, -1)
farPosition.applyMatrix4(camera.projectionMatrixInverse); // 相機坐標系:(0, 0, -near)

// 在相機空間,根據深度,按比例計算出相機空間x和y值。
const t = (cameraDepth - nearPosition.z) / (farPosition.z - nearPosition.z);

// 將交點從相機空間中的坐標,轉換到世界坐標系坐標。
world.set(
    nearPosition.x + (farPosition.x - nearPosition.x) * t,
    nearPosition.y + (farPosition.y - nearPosition.y) * t,
    cameraDepth
);
world.applyMatrix4(camera.matrixWorld);

 

完整代碼:

相關應用

使用gpu選取物體並計算交點位置,多用於需要性能非常高的情況。例如:

1. 鼠標移動到三維模型上的hover效果。

2. 添加模型時,模型隨着鼠標移動,實時預覽模型放到場景中的效果。

3. 距離測量、面積測量等工具,線條和多邊形隨着鼠標在平面上移動,實時預覽效果,並計算長度和面積。

4. 場景和模型非常大,光線投射法選取速度很慢,用戶體驗非常不好。

這裏給一個使用gpu選取物體和實現鼠標hover效果的圖片。紅色邊框是選取效果,黃色半透明效果是鼠標hover效果。

 

 

 

看不明白?可能你不太熟悉three.js中的各種投影運算。下面給出three.js中的投影運算公式。

 

three.js中的投影運算

1. modelViewMatrix = camera.matrixWorldInverse * object.matrixWorld

2. viewMatrix = camera.matrixWorldInverse

3. modelMatrix = object.matrixWorld

4. project = applyMatrix4( camera.matrixWorldInverse ).applyMatrix4( camera.projectionMatrix )

5. unproject = applyMatrix4( camera.projectionMatrixInverse ).applyMatrix4( camera.matrixWorld )

6. gl_Position = projectionMatrix * modelViewMatrix * position
                      = projectionMatrix * camera.matrixWorldInverse * matrixWorld * position
                      = projectionMatrix * viewMatrix * modelMatrix * position

 

參考資料:

1. 完整實現代碼:

2. 基於three.js的開源三維場景編輯器:

3. OpenGL中使用着色器繪製深度值:https://stackoverflow.com/questions/6408851/draw-the-depth-value-in-opengl-using-shaders

4. 在glsl中,獲取真實的片元着色器深度值:https://gamedev.stackexchange.com/questions/93055/getting-the-real-fragment-depth-in-glsl

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

SpringSecurity退出功能實現的正確方式

本文將介紹在Spring Security框架下如何實現用戶的”退出”logout的功能。其實這是一個非常簡單的功能,我見過很多的程序員在使用了Spring Security之後,仍然去自己寫controller方法實現logout功能,這種做法就好像耕地,你有机械設備你不用,你非要用牛。

一、logout最簡及最佳實踐

其實使用Spring Security進行logout非常簡單,只需要在spring Security配置類配置項上加上這樣一行代碼:http.logout()。關於spring Security配置類的其他很多實現、如:HttpBasic模式、formLogin模式、自定義登錄驗證結果、使用權限表達式、session會話管理,在本號的之前的文章已經都寫過了。本節的核心內容就是在原有配置的基礎上,加上這樣一行代碼:http.logout()。

@Configuration
@EnableWebSecurity
public class SecSecurityConfig extends WebSecurityConfigurerAdapter {
 
    @Override
    protected void configure(final HttpSecurity http) throws Exception {
        http.logout();
   }

}

加上logout配置之後,在你的“退出”按鈕上使用/logtou作為請求登出的路徑。

<a href="/logout" >退出</a>

logout功能我們就完成了。實際上的核心代碼只有兩行。

二、默認的logout做了什麼?

雖然我們簡簡單單的實現了logout功能,是不是還不足夠放心?我們下面就來看一下Spring Security默認在logout過程中幫我們做了哪些動作。

  • 當前session失效,即:logout的核心需求,session失效就是訪問權限的回收。
  • 刪除當前用戶的 remember-me“記住我”功能信息
  • clear清除當前的 SecurityContext
  • 重定向到登錄頁面,loginPage配置項指定的頁面

通常對於一個應用來講,以上動作就是logout功能所需要具備的功能了。

三、個性化配置

雖然Spring Security默認使用了/logout作為退出處理請求路徑,登錄頁面作為退出之後的跳轉頁面。這符合絕大多數的應用的開發邏輯,但有的時候我們需要一些個性化設置,如下:

 http.logout()
     .logoutUrl("/signout")
     .logoutSuccessUrl("/aftersignout.html")
     .deleteCookies("JSESSIONID")
  • 通過指定logoutUrl配置改變退出請求的默認路徑,當然html退出按鈕的請求url也要修改
  • 通過指定logoutSuccessUrl配置,來顯式指定退出之後的跳轉頁面
  • 還可以使用deleteCookies刪除指定的cookie,參數為cookie的名稱

四、LogoutSuccessHandler

如果上面的個性化配置,仍然滿足不了您的應用需求。可能您的應用需要在logout的時候,做一些特殊動作,比如登錄時長計算,清理業務相關的數據等等。你可以通過實現LogoutSuccessHandler 接口來實現你的業務邏輯。

@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
    
    @Override
    public void onLogoutSuccess(HttpServletRequest request, 
                                HttpServletResponse response, 
                                Authentication authentication) 
                                throws IOException, ServletException {
        //這裏書寫你自己的退出業務邏輯
        
        // 重定向到登錄頁
        response.sendRedirect("/login.html");
    }
}

然後進行配置使其生效,核心代碼就是一行logoutSuccessHandler。注意logoutSuccessUrl不要與logoutSuccessHandler一起使用,否則logoutSuccessHandler將失效。

@Configuration
@EnableWebSecurity
public class SecSecurityConfig extends WebSecurityConfigurerAdapter {
    
@Autowired
    private MyLogoutSuccessHandler myLogoutSuccessHandler;

    @Override
    protected void configure(final HttpSecurity http) throws Exception {
         http.logout()
             .logoutUrl("/signout")
             //.logoutSuccessUrl(``"/aftersignout.html"``)
             .deleteCookies("JSESSIONID")
              //自定義logoutSuccessHandler
             .logoutSuccessHandler(myLogoutSuccessHandler);   
   }
}

期待您的關注

  • 博主最近新寫了一本書:
  • 本文轉載註明出處(必須帶連接,不能只轉文字):。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

我的程序員之路——大學和2012年

  我於2007年參加高考,順利考入一所男女比例嚴重失調的師範大學,主修計算機科學與技術。其實高中的時候本來想選生物的,可惜報生物的人太少,就沒有開班,後面選修了化學。原計劃是想學高分子材料相關專業的,怎奈高考分數太低,沒有考上相關的大學。第二志願填寫的是計算機相關的學校,當初並不知道這個專業是編程,以為是修理電腦的。因為自家電腦老是這個那個的問題,所以就義無反顧的報考了計算機專業。填志願的時候沒有什麼偉大的理想,也沒有什麼周密的計劃,就是隨意的這麼一填,沒想到現在就靠這個吃飯了。

一、課程

1)疑惑

  本校的這門計算機科學與技術是偏向綜合的,既要學軟件相關的知識,又要學硬件相關的知識。剛進入大一的時候,對一些課程感到疑惑,例如數學、物理、英語、模擬電路等課程,修電腦怎麼要學這些課程,後來才知道,其實我們學的不是修電腦,而是編程。

2)為分數而學習

  由於對編程並不熱愛,因此上課其實也不是很上心,剛開始就是為了分數而學習,完全領會不到這些課程的意義。比較上心的就是C語言了,譚浩強出的那本。一開始完全沒有基礎,寫代碼很吃力,而且那時候筆記本電腦還沒普及,也很少有機會上機調試。雖然學校有機房,但離的比較遠,基本是不會去的。後面練習的多了,慢慢也就會了,應付考試是沒問題的,但寫的代碼不夠有靈性。大二學了數據結構,比較難理解,也是靠課後練習,當時純粹是應試,所以過段時間就都忘了。包括後面的網絡、離散數學、排列組合等等,都是為了考個好成績才學的。

3)學習氛圍

  大一和大二被安排在遠離繁華都市的偏僻海邊的新校區,在這裏沒有量販KTV、沒有大型超市、沒有巨型商場。此處雖然資源有限,但是卻非常適合學習。不過,大家高中時候都學累了,進入大學后就都很放鬆。很多人喜歡去網絡文化交流中心包夜,最誇張的是隔壁班的一個同學,居然一去就是幾個月。在這種環境中,想要心無雜念的深入學習計算機真的蠻難的,況且自己也不熱愛,所以經常告訴自己學這個只是為了以後能有個混口飯吃的技能。

  後面大三回到老校區,遇到了專升本的那幫人,他們的學習熱情與我們正好相反。他們真的是熱愛這個專業,有次放學路過,發現他們把任課老師圍了起來,正在向老師諮詢這個那個的問題,他們肯定是已經明白了學習的意義,所以才能這麼投入。

4)教授

  我們文科學院的教授講課都非常有趣,可以用引人入勝來形容,上他們的課相對會有勁很多,尤其是歷史課,可以聽到很多故事。反觀我們理科院的教授,就不那麼有趣了,很多時候都是蠻枯燥的,上課很容易走神。他們的水平都很高,就是講課的時候很難讓我們理解,當時有一門離散數學,讓我印象深刻,大部分人都不會。課堂氛圍比較好的是操作系統課的那位老師,他講的蠻通俗的,有時候上課還能互動互動。

5)操作課

  大二有一門網頁設計課,授課老師讓我們用Dreamwever製作一張靜態網頁,模板就是他的博客首頁,他博客的訪問量一度飆升。這門課提起了大家的興趣,都在宿舍開筆記本製作,還會對比,看誰做的相似度最高,有的人做的很不錯。看來大家還是喜歡這種能看到效果的操作課,像數據庫、數據結構那種理論課,都提不起大家的興趣。

  大三的時候,還有一門多媒體,這門課會教點PS的內容,讓我們整天P一個胖子,摳圖啥的,大家有時候還是會有點興趣。

6)ACM

  ACM是一項編程競賽,大一的時候,有學長過來做宣講,說拿到好名次能為以後找工作提供很大的便利,一下子就勾起了一大批人的興趣,大家爭相報名參加。因為名次的含金量高,所以這項編程競賽難度也很大。ACM考的是算法,並且他的題目都是英文的,有專門的刷題網站,很多人一看要做題目,興緻就降低了很多,後面又看到題目難度很大,有些題目有點奧數的味道,漸漸的越來越多的人選擇退出。我當時也堅持了一段時間,不過自己的數學建模能力實在太弱,最終也還是放棄了。隔壁班有個同學堅持了,還能拿到名次,畢業的時候直接進了騰訊。

二、實習

1)ERP系統

  大三下半年輔導員給了我一個機會,讓我和一個學長一起做個校外的項目,雖然這個項目做到一半終止了,但對我的影響是很大的。當時是用的軟件編輯器是VS2005,打算做個ERP系統,學長是在英特爾上班的,他把那套成熟的軟件開發模式帶了過來。第一次使用項目管理系統readmine,第一次使用版本控制系統SVN,第一次使用C#開發系統,第一次製作一個完整的項目,第一次採集需求等各種第一次,讓我感覺自己好像已經進入了職場。每個月還能有500塊的收入,我用第一個月的收入買了塊500G的移動硬盤。不得不說,這次實習經歷,直接改變了我未來的職業規劃,促使我踏上了程序員這條道路。

2)商城系統

  時間很快到了大四,那一年我的一個初中同學聯繫到了我,和我說他同學搞了個軟件項目,正好缺人,想讓我也加入。當時學長的那個ERP項目已經被腰斬,正好沒事,馬上就答應了。抽了一天時間,坐了很久的公交,到了他們租的一套公寓里,了解到他們想搞個商城系統,正好也是用C#開發,模仿當時的一套開源系統來做。這次的經歷讓我接觸到了前端,確切的說是JavaScript,因為CSS和HTML由團隊的另外一個成員做。與此同時,我也迷上了前端,因為在完成某個效果時,能帶給我巨大的成就感和滿足感。不過,一直到6年後,才有機會轉型成全職前端。

3)戶外旅遊網

  商城系統團隊後面由於種種客觀原因無奈解散了,當時正好有一家戶外旅遊網在我們大學里招實習生維護公司的網站。我就報名了,學校在徐匯區,而這家公司在虹口區,兩者相距蠻遠的,地鐵都要坐一個多小時,然後下地鐵再走到公司。這家公司還有另外一個同事管網站這塊,不過他只能算半個,因為他主要管旅遊產品那塊。舊網站是用ASP開發的,我過來后老闆讓我先做點邊邊角角的事情,然後讓我開發一個和開心網類似的社交網站。這次是獨立完成了這個項目,包括數據庫設計、頁面製作、產品規劃等,我的另一篇文章《》就詳細記錄了其中的開發過程。實習期間,還拿了公司傳單在學校里發,中午的時候,在人流量最多的地方發,對自己也是一種挑戰。

三、2012年

  轉眼四年過去了,我也畢業了。但一直到畢業的時候,還沒意識到大學四年是用來打基礎的,導致剛畢業那會兒軟件基礎很薄弱。

  實習的那家旅遊公司也和我簽約了,但由於個人原因,我辭掉了這份工作。經一個朋友推薦,我回到了家附近,位於上海郊區,在那裡找到了一家軟件外包公司。這家公司主要給政府做PPT和網站,規模很小,全公司只有8個人,我從那離職7年多了,另外那7人還在。

  這裏我建議剛畢業的學生有機會去規模大點的公司,還是要把握中,因為上規模的公司在組織架構、技術沉澱、規章制度等各方面都比較成熟,並且還有比較好的人脈網,社會終歸是由人組成的,你的人脈越廣,好的機會也會越多。

四、新的開始

1)面試

  2011年9月我來到了這家小外包公司面試,我記得那天是周末,過去的時候公司里一個人都沒的。公司老闆面的我,沒涉及多少技術,就說了當前開發用的是我比較熟悉的C#語言,還介紹了一下公司的業務,主要做些和政府相關的項目,然後就讓我第二天來上班了。這裏說個很巧的事情,公司周五有個同事離職,而我過來就是替代此人的,我結婚那天才發現這個同事就是我老婆關係很近的表姐,真是無巧不成書。

  這家公司很少加班,朝八晚五,基本到點就走,包中飯,老財務早上會去菜場買菜,然後在公司燒。活挺輕鬆的,不過就是工資太低,公積金也不交,試用期是2000一個月,轉正後也只有3500,就這樣我幹了一年半。

  其實當時還有另外一次面試,另一個朋友推薦的,一家大公司,在上海市中心南京西路上。我面試后感覺自己能力還不夠,並且離家太遠,當時不怎麼想太折騰,於是就婉拒了他們的複試。安心的在這家小公司鍛煉,希望能快速的成長。

2)上手

  說個題外話,剛進入這家公司的時候,我開通了個人博客,不過對自己不夠自信,怕被別人嘲諷,一直到3年後的2014年才撰寫了自己的第一篇博文。其實現在想想,平時寫點技術和項目的總結,對自己的成長會有很大的幫助。

  剛開始給我安排的都是些遺留項目,就是簡單的改改頁面中的細節,難度不大。有一次,老闆問我壓力大不大,我很爽快的回答不大。後面讓我獨立的完成一些項目,總體來說沒有什麼大難度。政府項目都比較有規律,後台的模塊大部分都能套用,前台的頁面只是換個皮膚,大框架也比較類似。不過,這段時間對CSS、HTML和JavaScript有了新的認識,公司真正意義上的開發除了我就是另外一個同事,因此很多時候做特效都得自己想辦法解決。這段時間搜索引擎發揮了巨大的作用,公司有段時間不能上百度,google又上不去,就改用了Bing,搜索質量感覺比百度要好一點。

3)挑戰

  要說這段時間比較有挑戰的項目應該就是一個重陽節登高的報名活動,就是個表單頁面,然後填手機號、姓名等信息,最後返回一個報名號給用戶。這個頁面的併發量比較高,5000個報名量基本在兩三個小時內就能全部搶完,對於我這個菜鳥來說,要處理這並不算高的併發還是有點挑戰的。我清晰的記得上線前的一天晚上輾轉反側,很擔心會出大事故,像頁面打不開、報錯等等,因為這邊沒有專業的測試,全憑自己測試,這就很難保證質量。還好,沒出大事故,但還是出現了兩個或多個領到了同一個報名號的問題,最後另外兩個同事一個個的打電話通知他們,換了新號碼給他們,這件事就算這麼過去了。

  活動上線后的第二天,和別人聊天的時候,他正好提到了這個活動,我跟他說這個活動我做的,還是蠻自豪的。

4)跑客戶

  外包公司免不了要跑到客戶那邊去,了解需求或修改BUG。有些客戶就在附近,走過去就行,有些就比較遠了,不僅如此,遇到颳風下雨烈日的天氣,還得跑出去,日晒雨淋的還是蠻苦的。

  有一次蠻坑的,跑到長寧區,基本一個下午就沒了,背個電腦過去,然後發現是他們Excel模板用的不對,只能呵呵了,再跑回來,基本已經快到五點下班時間了,一天就沒了。還有一次跑到太倉去,老闆想開發新客戶,然後當天開車來回,有時候回公司已經六七點了。我運氣比較好,遇到的客戶都是蠻客氣的,也很配合,聽說太倉那個客戶,後面有個老頭總是刁難我們。

5)離職

  我離職的主要原因還是工資的問題,實在太低,後面有個朋友找我,他那邊在創業,有個很好的項目,讓我過去幫忙,工資還開雙倍,我馬上就答應了。這個時候是2012年的12月份,馬上要過年了,雖然有點年終獎,但很少,所以也就不在意了。

  與公司同事相處的還是很融洽的,他們也都很理解我,我在離職前特地請大家去吃了頓小肥羊火鍋,算是散夥飯。今年技術有所提升,人際關係的處理上也愈加成熟。

五、兼職

  這家公司的活蠻輕鬆的,上家實習的旅遊公司又不想再去外面招人,就找到了我,讓我兼職乾著,每個月給個固定的2000元。

1)職能

  主要就是維護網站,其中最忙的是兩次改版。尤其是2011年的12月份,我晚上下班后改頁面,周末去虹口和公司的人對需求,對頁面,那段時間非常的辛苦。當時很年輕,也不覺得,只感覺自己的生活很充實。但有時候,我白天上班的時候旅遊網出了問題,就只能遠程修改一下了。有一次最嚴重,周五的時候,頁面打不開了,下午就請假,直接打了200多的車過去,然後周末就一直在那邊改代碼。現在讓我做兼職我肯定是不願意的。

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

[UWP]用Win2D實現鏤空文字

1. 前言

之前用PointLight做了一個番茄鍾,效果還不錯,具體可見這篇文章:

後來試玩了Win2D,這次就用Win2D實現文字的鏤空效果,配合PointLight做一個內斂不張揚的番茄鍾。

實現鏤空文字的核心思想是使用CanvasGeometry.CreateText從TextLayout獲取一個Geometry,然後使用DrawGeometry將它畫到DrawingSurface。這篇文章介紹了具體的實現步驟。

2. 參考例子

Win2D Gallery提供了大量Win2D的Sample,這次就參考了其中的文字鏤空效果例子,地址和運行效果如下:

3. 實現步驟

Sample的代碼量雖多,其實核心並不複雜,下面講講需要用到的API:

3.1 CanvasDevice.GetSharedDevice

因為要用到Win2D,所以首先要引用 nuget包。因為我的目標不是輸出到CanvasControl上,而是想要輸出到一個SpriteVisual上,所以使用:

var canvasDevice = CanvasDevice.GetSharedDevice();

3.2 CanvasComposition.CreateCompositionGraphicsDevice

然後創建一個Compositor,並將這個Compositor和CanvasDevice關聯起來,這裏需要使用 創建 :

var compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
var graphicsDevice = CanvasComposition.CreateCompositionGraphicsDevice(compositor, canvasDevice);

3.3 CompositionGraphicsDevice.CreateDrawingSurface

然後使用創建一個對象,它是用來繪畫內容的表面:

var drawingSurface = graphicsDevice.CreateDrawingSurface(e.NewSize, DirectXPixelFormat.B8G8R8A8UIntNormalized, DirectXAlphaMode.Premultiplied);

3.4 Compositor.CreateSurfaceBrush

使用創建一個CompositionSurfaceBrush,它的作用是使用像素繪製SpriteVisual,簡單來說它就是一張位圖,然後輸出到SpriteVisual上:

var maskSurfaceBrush = compositor.CreateSurfaceBrush(drawingSurface);
spriteTextVisual.Brush = maskSurfaceBrush;

3.5 CanvasComposition.CreateDrawingSession

有了CompositionDrawingSurface就可以為所欲為了,將這個DrawingSurface作為參數,調用創建,DrawingSession提供了多個函數,可以自由地在DrawingSurface上畫文字、形狀、圖片甚至SVG。

using (var session = CanvasComposition.CreateDrawingSession(drawingSurface))
{

}

3.6 CanvasTextFormat和CanvasTextLayout

要再DrawingSurface上寫字,需要,而CanvasTextLayout中的文字大小、格式等則由定義:

using (var textFormat = new CanvasTextFormat()
{
    FontSize = (float)FontSize,
    Direction = CanvasTextDirection.LeftToRightThenTopToBottom,
    VerticalAlignment = CanvasVerticalAlignment.Center,
    HorizontalAlignment = CanvasHorizontalAlignment.Center,

})
{
    using (var textLayout = new CanvasTextLayout(session, Text, textFormat, width, height))
    {
        Color fontColor = FontColor;
        session.DrawTextLayout(textLayout, 0, 0, fontColor);
    }
}

3.7 CanvasGeometry.CreateText

因為我的目標是鏤空的文字,所以不能直接使用DrawTextLayout。這裏需要使用從TextLayout獲取一個Geometry,然後使用DrawGeometry將它畫到DrawingSurface。CanvasStrokeStyle是可選的,它控制邊框的虛線。

using (var textGeometry = CanvasGeometry.CreateText(textLayout))
{
    var dashedStroke = new CanvasStrokeStyle()
    {
        DashStyle = DashStyle
    };
    session.DrawGeometry(textGeometry, OutlineColor, (float)StrokeWidth, dashedStroke);
}

4. 封裝為控件

將上面的代碼總結一下,封裝為一個OutlineTextControl 控件,它提供了Text、OutlineColor、FontColor等屬性,在控件SizeChanged時,或者各個屬性改變時調用DrawText重新在CompositionDrawingSurface上繪製文字。代碼大致如下:

public class OutlineTextControl : Control
{
    private CompositionDrawingSurface _drawingSurface;

    public OutlineTextControl()
    {
        var compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
        var graphicsDevice = CanvasComposition.CreateCompositionGraphicsDevice(compositor, CanvasDevice.GetSharedDevice());
        var spriteTextVisual = compositor.CreateSpriteVisual();

        ElementCompositionPreview.SetElementChildVisual(this, spriteTextVisual);
        SizeChanged += (s, e) =>
        {
            _drawingSurface = graphicsDevice.CreateDrawingSurface(e.NewSize, DirectXPixelFormat.B8G8R8A8UIntNormalized, DirectXAlphaMode.Premultiplied);
            DrawText();
            var maskSurfaceBrush = compositor.CreateSurfaceBrush(_drawingSurface);
            spriteTextVisual.Brush = maskSurfaceBrush;
            spriteTextVisual.Size = e.NewSize.ToVector2();
        };
        RegisterPropertyChangedCallback(FontSizeProperty, new DependencyPropertyChangedCallback((s, e) =>
        {
            DrawText();
        }));
    }


    private void DrawText()
    {
        if (ActualHeight == 0 || ActualWidth == 0 || string.IsNullOrWhiteSpace(Text) || _drawingSurface == null)
            return;

        var width = (float)ActualWidth;
        var height = (float)ActualHeight;
        using (var session = CanvasComposition.CreateDrawingSession(_drawingSurface))
        {
            session.Clear(Colors.Transparent);
            using (var textFormat = new CanvasTextFormat()
            {
                FontSize = (float)FontSize,
                Direction = CanvasTextDirection.LeftToRightThenTopToBottom,
                VerticalAlignment = CanvasVerticalAlignment.Center,
                HorizontalAlignment = CanvasHorizontalAlignment.Center,

            })
            {
                using (var textLayout = new CanvasTextLayout(session, Text, textFormat, width, height))
                {
                    if (ShowNonOutlineText)
                    {
                        session.DrawTextLayout(textLayout, 0, 0, FontColor);
                    }

                    using (var textGeometry = CanvasGeometry.CreateText(textLayout))
                    {
                        var dashedStroke = new CanvasStrokeStyle()
                        {
                            DashStyle = DashStyle
                        };
                        session.DrawGeometry(textGeometry, OutlineColor, (float)StrokeWidth, dashedStroke);
                    }
                }
            }
        }
    }

//SOME CODE AND PROPERTIES

}

5. 結語

文章開頭的那個番茄鍾源碼可以在這裏查看:

也可以安裝我的番茄鍾應用試玩一下,安裝地址:

6. 參考

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

[springboot 開發單體web shop] 8. 商品詳情&評價展示

上文回顧

我們實現了根據搜索關鍵詞查詢商品列表和根據商品分類查詢,並且使用到了mybatis-pagehelper插件,講解了如何使用插件來幫助我們快速實現分頁數據查詢。本文我們將繼續開發商品詳情頁面和商品留言功能的開發。

需求分析

關於商品詳情頁,和往常一樣,我們先來看一看jd的示例:

從上面2張圖,我們可以看出來,大體上需要展示給用戶的信息。比如:商品圖片,名稱,價格,等等。在第二張圖中,我們還可以看到有一個商品評價頁簽,這些都是我們本節要實現的內容。

商品詳情

開發梳理

我們根據上圖(權當是需求文檔,很多需求文檔寫的比這個可能還差勁很多…)分析一下,我們的開發大致都要關注哪些points:

  • 商品標題
  • 商品圖片集合
  • 商品價格(原價以及優惠價)
  • 配送地址(我們的實現不在此,我們後續直接實現在下單邏輯中)
  • 商品規格
  • 商品分類
  • 商品銷量
  • 商品詳情
  • 商品參數(生產場地,日期等等)

根據我們梳理出來的信息,接下來開始編碼就會很簡單了,大家可以根據之前課程講解的,先自行實現一波,請開始你們的表演~

編碼實現

DTO實現

因為我們在實際的數據傳輸過程中,不可能直接把我們的數據庫entity之間暴露到前端,而且我們商品相關的數據是存儲在不同的數據表中,我們必須要封裝一個ResponseDTO來對數據進行傳遞。

  • ProductDetailResponseDTO包含了商品主表信息,以及圖片列表、商品規格(不同SKU)以及商品具體參數(產地,生產日期等信息)
@Data
@ToString
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ProductDetailResponseDTO {
    private Products products;
    private List<ProductsImg> productsImgList;
    private List<ProductsSpec> productsSpecList;
    private ProductsParam productsParam;
}

Custom Mapper實現

根據我們之前表的設計,這裏使用生成的通用mapper就可以滿足我們的需求。

Service實現

從我們封裝的要傳遞到前端的ProductDetailResponseDTO就可以看出,我們可以根據商品id分別查詢出商品的相關信息,在controller進行數據封裝就可以了,來實現我們的查詢接口。

  • 查詢商品主表信息(名稱,內容等)

    com.liferunner.service.IProductService中添加接口方法:

        /**
         * 根據商品id查詢商品
         *
         * @param pid 商品id
         * @return 商品主信息
         */
        Products findProductByPid(String pid);

    接着,在com.liferunner.service.impl.ProductServiceImpl中添加實現方法:

        @Override
        @Transactional(propagation = Propagation.SUPPORTS)
        public Products findProductByPid(String pid) {
            return this.productsMapper.selectByPrimaryKey(pid);
        }

    直接使用通用mapper根據主鍵查詢就可以了。

    同上,我們依次來實現圖片、規格、以及商品參數相關的編碼工作

  • 查詢商品圖片信息列表

        /**
         * 根據商品id查詢商品規格
         *
         * @param pid 商品id
         * @return 規格list
         */
        List<ProductsSpec> getProductSpecsByPid(String pid);
    
    ----------------------------------------------------------------
    
        @Override
        public List<ProductsSpec> getProductSpecsByPid(String pid) {
            Example example = new Example(ProductsSpec.class);
            val condition = example.createCriteria();
            condition.andEqualTo("productId", pid);
            return this.productsSpecMapper.selectByExample(example);
        }
  • 查詢商品規格列表

        /**
         * 根據商品id查詢商品規格
         *
         * @param pid 商品id
         * @return 規格list
         */
        List<ProductsSpec> getProductSpecsByPid(String pid);
    
    ------------------------------------------------------------------
    
        @Override
        public List<ProductsSpec> getProductSpecsByPid(String pid) {
            Example example = new Example(ProductsSpec.class);
            val condition = example.createCriteria();
            condition.andEqualTo("productId", pid);
            return this.productsSpecMapper.selectByExample(example);
        }
  • 查詢商品參數信息

        /**
         * 根據商品id查詢商品參數
         *
         * @param pid 商品id
         * @return 參數
         */
        ProductsParam findProductParamByPid(String pid);
    
    ------------------------------------------------------------------
    
        @Override
        public ProductsParam findProductParamByPid(String pid) {
            Example example = new Example(ProductsParam.class);
            val condition = example.createCriteria();
            condition.andEqualTo("productId", pid);
            return this.productsParamMapper.selectOneByExample(example);
        }

Controller實現

在上面將我們需要的信息查詢實現之後,然後我們需要在controller對數據進行包裝,之後再返回到前端,供用戶來進行查看,在com.liferunner.api.controller.ProductController中添加對外接口/detail/{pid},實現如下:

    @GetMapping("/detail/{pid}")
    @ApiOperation(value = "根據商品id查詢詳情", notes = "根據商品id查詢詳情")
    public JsonResponse findProductDetailByPid(
        @ApiParam(name = "pid", value = "商品id", required = true)
        @PathVariable String pid) {
        if (StringUtils.isBlank(pid)) {
            return JsonResponse.errorMsg("商品id不能為空!");
        }
        val product = this.productService.findProductByPid(pid);
        val productImgList = this.productService.getProductImgsByPid(pid);
        val productSpecList = this.productService.getProductSpecsByPid(pid);
        val productParam = this.productService.findProductParamByPid(pid);
        val productDetailResponseDTO = ProductDetailResponseDTO
            .builder()
            .products(product)
            .productsImgList(productImgList)
            .productsSpecList(productSpecList)
            .productsParam(productParam)
            .build();
        log.info("============查詢到商品詳情:{}==============", productDetailResponseDTO);

        return JsonResponse.ok(productDetailResponseDTO);
    }

從上述代碼中可以看到,我們分別查詢了商品、圖片、規格以及參數信息,使用ProductDetailResponseDTO.builder().build()封裝成返回到前端的對象。

Test API

按照慣例,寫完代碼我們需要進行測試。

{
  "status": 200,
  "message": "OK",
  "data": {
    "products": {
      "id": "smoke-100021",
      "productName": "(奔跑的人生) - 中華",
      "catId": 37,
      "rootCatId": 1,
      "sellCounts": 1003,
      "onOffStatus": 1,
      "createdTime": "2019-09-09T06:45:34.000+0000",
      "updatedTime": "2019-09-09T06:45:38.000+0000",
      "content": "吸煙有害健康“
    },
    "productsImgList": [
      {
        "id": "1",
        "productId": "smoke-100021",
        "url": "http://www.life-runner.com/product/smoke/img1.png",
        "sort": 0,
        "isMain": 1,
        "createdTime": "2019-07-01T06:46:55.000+0000",
        "updatedTime": "2019-07-01T06:47:02.000+0000"
      },
      {
        "id": "2",
        "productId": "smoke-100021",
        "url": "http://www.life-runner.com/product/smoke/img2.png",
        "sort": 1,
        "isMain": 0,
        "createdTime": "2019-07-01T06:46:55.000+0000",
        "updatedTime": "2019-07-01T06:47:02.000+0000"
      },
      {
        "id": "3",
        "productId": "smoke-100021",
        "url": "http://www.life-runner.com/product/smoke/img3.png",
        "sort": 2,
        "isMain": 0,
        "createdTime": "2019-07-01T06:46:55.000+0000",
        "updatedTime": "2019-07-01T06:47:02.000+0000"
      }
    ],
    "productsSpecList": [
      {
        "id": "1",
        "productId": "smoke-100021",
        "name": "中華",
        "stock": 2276,
        "discounts": 1.00,
        "priceDiscount": 7000,
        "priceNormal": 7000,
        "createdTime": "2019-07-01T06:54:20.000+0000",
        "updatedTime": "2019-07-01T06:54:28.000+0000"
      },
    ],
    "productsParam": {
      "id": "1",
      "productId": "smoke-100021",
      "producPlace": "中國",
      "footPeriod": "760天",
      "brand": "中華",
      "factoryName": "中華",
      "factoryAddress": "陝西",
      "packagingMethod": "盒裝",
      "weight": "100g",
      "storageMethod": "常溫",
      "eatMethod": "",
      "createdTime": "2019-05-01T09:38:30.000+0000",
      "updatedTime": "2019-05-01T09:38:34.000+0000"
    }
  },
  "ok": true
}

商品評價

在文章一開始我們就看過jd詳情頁面,有一個詳情頁簽,我們來看一下:

它這個實現比較複雜,我們只實現相對重要的幾個就可以了。

開發梳理

針對上圖中紅色方框圈住的內容,分別有:

  • 評價總數
  • 好評度(根據好評總數,中評總數,差評總數計算得出)
  • 評價等級
  • 以及用戶信息加密展示
  • 評價內容

我們來實現上述分析的相對必要的一些內容。

編碼實現

查詢評價

根據我們需要的信息,我們需要從用戶表、商品表以及評價表中來聯合查詢數據,很明顯單表通用mapper無法實現,因此我們先來實現自定義查詢mapper,當然數據的傳輸對象是我們需要先來定義的。

Response DTO實現

創建com.liferunner.dto.ProductCommentDTO.

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ProductCommentDTO {
    //評價等級
    private Integer commentLevel;
    //規格名稱
    private String specName;
    //評價內容
    private String content;
    //評價時間
    private Date createdTime;
    //用戶頭像
    private String userFace;
    //用戶昵稱
    private String nickname;
}

Custom Mapper實現

com.liferunner.custom.ProductCustomMapper中添加查詢接口方法:

    /***
     * 根據商品id 和 評價等級查詢評價信息
     * <code>
     *         Map<String, Object> paramMap = new HashMap<>();
     *         paramMap.put("productId", pid);
     *         paramMap.put("commentLevel", level);
     *</code>
     * @param paramMap
     * @return java.util.List<com.liferunner.dto.ProductCommentDTO>
     * @throws
     */
    List<ProductCommentDTO> getProductCommentList(@Param("paramMap") Map<String, Object> paramMap);

mapper/custom/ProductCustomMapper.xml中實現該接口方法的SQL:

    <select id="getProductCommentList" resultType="com.liferunner.dto.ProductCommentDTO" parameterType="Map">
        SELECT
        pc.comment_level as commentLevel,
        pc.spec_name as specName,
        pc.content as content,
        pc.created_time as createdTime,
        u.face as userFace,
        u.nickname as nickname
        FROM items_comments pc
        LEFT JOIN users u
        ON pc.user_id = u.id
        WHERE pc.item_id = #{paramMap.productId}
        <if test="paramMap.commentLevel != null and paramMap.commentLevel != ''">
            AND pc.comment_level = #{paramMap.commentLevel}
        </if>
    </select>

如果沒有傳遞評價級別的話,默認查詢全部評價信息。

Service 實現

com.liferunner.service.IProductService中添加查詢接口方法:

    /**
     * 查詢商品評價
     *
     * @param pid        商品id
     * @param level      評價級別
     * @param pageNumber 當前頁碼
     * @param pageSize   每頁展示多少條數據
     * @return 通用分頁結果視圖
     */
    CommonPagedResult getProductComments(String pid, Integer level, Integer pageNumber, Integer pageSize);

com.liferunner.service.impl.ProductServiceImpl實現該方法:

    @Override
    public CommonPagedResult getProductComments(String pid, Integer level, Integer pageNumber, Integer pageSize) {
        Map<String, Object> paramMap = new HashMap<>();
        paramMap.put("productId", pid);
        paramMap.put("commentLevel", level);
        // mybatis-pagehelper
        PageHelper.startPage(pageNumber, pageSize);
        val productCommentList = this.productCustomMapper.getProductCommentList(paramMap);
        for (ProductCommentDTO item : productCommentList) {
            item.setNickname(SecurityTools.HiddenPartString4SecurityDisplay(item.getNickname()));
        }
        // 獲取mybatis插件中獲取到信息
        PageInfo<?> pageInfo = new PageInfo<>(productCommentList);
        // 封裝為返回到前端分頁組件可識別的視圖
        val commonPagedResult = CommonPagedResult.builder()
                .pageNumber(pageNumber)
                .rows(productCommentList)
                .totalPage(pageInfo.getPages())
                .records(pageInfo.getTotal())
                .build();
        return commonPagedResult;
    }

因為評價過多會使用到分頁,這裏使用通用分頁返回結果,關於分頁,可查看。

Controller實現

com.liferunner.api.controller.ProductController中添加對外查詢接口:

    @GetMapping("/comments")
    @ApiOperation(value = "查詢商品評價", notes = "根據商品id查詢商品評價")
    public JsonResponse getProductComment(
        @ApiParam(name = "pid", value = "商品id", required = true)
        @RequestParam String pid,
        @ApiParam(name = "level", value = "評價級別", required = false, example = "0")
        @RequestParam Integer level,
        @ApiParam(name = "pageNumber", value = "當前頁碼", required = false, example = "1")
        @RequestParam Integer pageNumber,
        @ApiParam(name = "pageSize", value = "每頁展示記錄數", required = false, example = "10")
        @RequestParam Integer pageSize
    ) {
        if (StringUtils.isBlank(pid)) {
            return JsonResponse.errorMsg("商品id不能為空!");
        }
        if (null == pageNumber || 0 == pageNumber) {
            pageNumber = DEFAULT_PAGE_NUMBER;
        }
        if (null == pageSize || 0 == pageSize) {
            pageSize = DEFAULT_PAGE_SIZE;
        }
        log.info("============查詢商品評價:{}==============", pid);

        val productComments = this.productService.getProductComments(pid, level, pageNumber, pageSize);
        return JsonResponse.ok(productComments);
    }

FBI WARNING:

@ApiParam(name = “level”, value = “評價級別”, required = false, example = “0”)
@RequestParam Integer level
關於ApiParam參數,如果接收參數為非字符串類型,一定要定義example為對應類型的示例值,否則Swagger在訪問過程中會報example轉換錯誤,因為example缺省為””空字符串,會轉換失敗。例如我們刪除掉level這個字段中的example=”0“,如下為錯誤信息(但是並不影響程序使用。)

2019-11-23 15:51:45 WARN  AbstractSerializableParameter:421 - Illegal DefaultValue null for parameter type integer
java.lang.NumberFormatException: For input string: ""
    at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
    at java.lang.Long.parseLong(Long.java:601)
    at java.lang.Long.valueOf(Long.java:803)
    at io.swagger.models.parameters.AbstractSerializableParameter.getExample(AbstractSerializableParameter.java:412)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:688)
    at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:721)
    at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:166)
    at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serializeContents(IndexedListSerializer.java:119)

Test API

福利講解

添加Propagation.SUPPORTS和不加的區別

有心的小夥伴肯定又注意到了,在Service中處理查詢時,我一部分使用了@Transactional(propagation = Propagation.SUPPORTS),一部分查詢又沒有添加事務,那麼這兩種方式有什麼不一樣呢?接下來,我們來揭開神秘的面紗。

  • Propagation.SUPPORTS

      /**
       * Support a current transaction, execute non-transactionally if none exists.
       * Analogous to EJB transaction attribute of the same name.
       * <p>Note: For transaction managers with transaction synchronization,
       * {@code SUPPORTS} is slightly different from no transaction at all,
       * as it defines a transaction scope that synchronization will apply for.
       * As a consequence, the same resources (JDBC Connection, Hibernate Session, etc)
       * will be shared for the entire specified scope. Note that this depends on
       * the actual synchronization configuration of the transaction manager.
       * @see org.springframework.transaction.support.AbstractPlatformTransactionManager#setTransactionSynchronization
       */
      SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),

    主要關注Support a current transaction, execute non-transactionally if none exists.從字面意思來看,就是如果當前環境有事務,我就加入到當前事務;如果沒有事務,我就以非事務的方式執行。從這方面來看,貌似我們加不加這一行其實都沒啥差別。

    划重點:NOTE,對於一個帶有事務同步的管理器來說,這裡有一丟丟的小區別啦。(所以大家在讀註釋的時候,一定要看這個Note.往往這裏面會有好東西給我們,就相當於我們的大喇叭!)

    這個同步事務管理器定義了一個事務同步的一個範圍,如果加了這個註解,那麼就等同於我讓你來管我啦,你裏面的資源我想用就可以用(JDBC Connection, Hibernate Session).

結論1

SUPPORTS 標註的方法可以獲取和當前事務環境一致的 Connection 或 Session,不使用的話一定是一個新的連接;
再注意下面又一個NOTE,即便上面的配置加入了,但是事務管理器的實際同步配置會影響到真實的執行到底是否會用你。看它的說明:@see org.springframework.transaction.support.AbstractPlatformTransactionManager#setTransactionSynchronization.

  /**
   * Set when this transaction manager should activate the thread-bound
   * transaction synchronization support. Default is "always".
   * <p>Note that transaction synchronization isn't supported for
   * multiple concurrent transactions by different transaction managers.
   * Only one transaction manager is allowed to activate it at any time.
   * @see #SYNCHRONIZATION_ALWAYS
   * @see #SYNCHRONIZATION_ON_ACTUAL_TRANSACTION
   * @see #SYNCHRONIZATION_NEVER
   * @see TransactionSynchronizationManager
   * @see TransactionSynchronization
   */
  public final void setTransactionSynchronization(int transactionSynchronization) {
      this.transactionSynchronization = transactionSynchronization;
  }

描述信息只是說在同一個事務管理器才能起作用,並沒有什麼實際意義,我們來看一下TransactionSynchronization具體的內容:

package org.springframework.transaction.support;

import java.io.Flushable;

public interface TransactionSynchronization extends Flushable {

  /** Completion status in case of proper commit. */
  int STATUS_COMMITTED = 0;

  /** Completion status in case of proper rollback. */
  int STATUS_ROLLED_BACK = 1;

  /** Completion status in case of heuristic mixed completion or system errors. */
  int STATUS_UNKNOWN = 2;

  /**
   * Suspend this synchronization.
   * Supposed to unbind resources from TransactionSynchronizationManager if managing any.
   * @see TransactionSynchronizationManager#unbindResource
   */
  default void suspend() {
  }

  /**
   * Resume this synchronization.
   * Supposed to rebind resources to TransactionSynchronizationManager if managing any.
   * @see TransactionSynchronizationManager#bindResource
   */
  default void resume() {
  }

  /**
   * Flush the underlying session to the datastore, if applicable:
   * for example, a Hibernate/JPA session.
   * @see org.springframework.transaction.TransactionStatus#flush()
   */
  @Override
  default void flush() {
  }

  /**
   * ...
   */
  default void beforeCommit(boolean readOnly) {
  }

  /**
   * ...
   */
  default void beforeCompletion() {
  }

  /**
   * ...
   */
  default void afterCommit() {
  }

  /**
   * ...
   */
  default void afterCompletion(int status) {
  }
}

事務管理器可以通過org.springframework.transaction.support.AbstractPlatformTransactionManager#setTransactionSynchronization(int)來對當前事務進行行為干預,比如將它設置為1,可以執行事務回調,設置為2,表示出錯了,但是如果沒有加入PROPAGATION.SUPPORTS註解的話,即便你在當前事務中,你也不能對我進行操作和變更。

結論2

添加PROPAGATION.SUPPORTS之後,當前查詢中可以對當前的事務進行設置回調動作,不添加就不行。

源碼下載

下節預告

下一節我們將繼續開發商品詳情展示以及商品評價業務,在過程中使用到的任何開發組件,我都會通過專門的一節來進行介紹的,兄弟們末慌!

gogogo!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

Docker從入門到掉坑(三):容器太多,操作好麻煩

前邊的兩篇文章裏面,我們講解了基於docker來部署基礎的SpringBoot容器,如果閱讀本文之前沒有相關基礎的話,可以回看之前的教程。

不知道大家在初次使用docker的時候是否有遇到這種場景,每次部署微服務都是需要執行docker run xxx,docker kill xxx 等命令來操作容器。假設說一個系統中依賴了多個docker容器,那麼對於每個docker容器的部署豈不是都需要手動編寫命令來啟動和關閉,這樣做就會增加運維人員的開發工作量,同時也容易出錯。

Docker Compose 編排技術

在前邊的文章中,我們講解了Docker容器化技術的發展,但是隨着我們的Docker越來越多的時候,對於容器的管理也是特別麻煩,因此Docker Compose技術也就誕生了。

Docker Compose技術是通過一份文件來定義和運行一系列複雜應用的Docker工具,通過Docker-compose文件來啟動多個容器,網上有很多關於Docker-compose的實戰案例,但是都會有些細節地方有所遺漏,所以下邊我將通過一個簡單的案例一步步地帶各位從淺入深地對Docker-compose進行學習。

基於Docker Compose來進行對SpringBoot微服務應用的打包集成

我們還是按照老樣子來構建一套基礎的SpringBoot微服務項目,首先我們來看看基礎版本的項目結構:

首先是我們pom文件的配置內容:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.sise.idea</groupId>
    <artifactId>springboot-docker</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>spring-boot-docker</name>
    <url>http://maven.apache.org</url>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.3.RELEASE</version>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.18</version>
        </dependency>
    </dependencies>

    <build>
        <finalName>springboot-docker</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

 

然後是java程序的內容代碼,這裏面有常規的controller,application類,代碼如下所示:

啟動類Application

package com.sise.docker;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author idea
 * @data 2019/11/20
 */
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }
}

 

控制器 DockerController

package com.sise.docker.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author idea
 * @data 2019/11/20
 */
@RestController
@RequestMapping(value = "/docker")
public class DockerController {


    @GetMapping(value = "/test")
    public String test(){
        System.out.println("=========docker test=========");
        return "this is docker test";
    }
}

 

yml配置文件:

server:
  port: 7089

 

接下來便是docker-compose打包時候要用到的配置文件了。這裏採用的方式通常都是針對必要的docker容器編寫一份dockerfile,然後統一由Docker Compose進行打包管理,假設我們的微服務中需要引用到了MySQL,MongoDB等應用,那麼整體架構如下圖所示:

那麼我們先從簡單的單個容器入手,看看該如何對SpringBoot做Docker Compose的管理,下邊是一份打包SpringBoot進入Docker容器的Dockerfile文件:

#需要依賴的其他鏡像
FROM openjdk:8-jdk-alpine
# Spring Boot應用程序為Tomcat創建的默認工作目錄。作用是在你的主機”/var/lib/docker”目錄下創建一個臨時的文件,並且鏈接到容器中#的”/tmp”目錄。
VOLUME /tmp

#是指將原先的src文件 添加到我們需要打包的鏡像裏面
ADD target/springboot-docker.jar app.jar

#設置鏡像的時區,避免出現8小時的誤差
ENV TZ=Asia/Shanghai

#容器暴露的端口號 和SpringBoot的yml文件暴露的端口號要一致
EXPOSE 7089

#輸入的啟動參數內容 下邊這段內容相當於運行了java -Xms256m -Xmx512m -jar app.jar 
ENTRYPOINT ["java","-Xms256m","-Xmx512m","-jar","app.jar"]

 

接着便是加入docker-compose.yml文件的環節了,下邊是腳本的內容:

#docker引擎對應所支持的docker-compose文本格式
version: '3'
services:

  #服務的名稱
  springboot-docker:
    build:
      context: .
      # 構建這個容器時所需要使用的dockerfile文件
      dockerfile: springboot-dockerfile
    ports:
      # docker容器和宿主機之間的端口映射
      - "7089:7089"

 

docker-compose.ym配置文件有着特殊的規則,通常我們都是先定義version版本號,然後便是列舉一系列與容器相關的services內容。

接下來將這份docker服務進行打包,部署到相關的linux服務器上邊,這裏我採用的是一台阿里雲上邊購買的服務器來演示。

目前該文件還沒有進行打包處理,所以沒有target目錄,因此dockerfile文件構建的時候是不會成功的,因此需要先進行mvn的打包:

mvn package

 

接着便是進行Docker-Compose命令的輸入了:

[root@izwz9ic9ggky8kub9x1ptuz springboot-docker]# docker-compose up -d
Starting springboot-docker_springboot-docker_1 ... done
[root@izwz9ic9ggky8kub9x1ptuz springboot-docker]# 

 

你會發現這次輸入的命令和之前教程中提及的docker指令有些出入,變成了docker-compose 指令,這條指令是專門針對Docker compose文件所設計的,加入了一個-d的參數用於表示後台運行該容器。由於我們的docker-compose文件中知識編寫了對於SpringBoot容器的打包,因此啟動的時候只會显示一個docker容器。

為了驗證docker-compose指令是否生效,我們可以通過docker–compose ps命令來進行驗證。

這裏邊我們使用 docker logs [容器id] 指令可以進入容器查看日誌的打印情況:

 

docker logs ad83c82b014d

 

最後我們通過請求之前寫好的接口便會看到相關的響應:

 

基礎版本的SpringBoot+Docker compose案例已經搭建好了,還記得我在開頭畫的那張圖片嗎:

通常在實際開發中,我們所面對的docker容器並不是那麼的簡單,還有可能會依賴到多個容器,那麼這個時候該如何來編寫docker compose文件呢?

下邊我們對原先的SpringBoot項目增加對於MySQLMongoDB的依賴,為了方便下邊的場景模擬,這裏我們增加兩個實體類:

用戶類

package com.sise.docker.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author idea
 * @data 2019/11/23
 */
@AllArgsConstructor
@NoArgsConstructor
@Data
public class User {

    private Integer id;

    private String username;
}

 

汽車類:

package com.sise.docker.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;

/**
 * @author idea
 * @data 2019/11/23
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Car {

    @Id
    private Integer id;

    private String number;
}

 

增加對於mongodb,mysql的pom依賴內容

 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.21</version>
        </dependency>

 

編寫相關的dao層:

package com.sise.docker.dao;

import com.sise.docker.domain.Car;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;

/**
 * @author idea
 * @data 2019/11/23
 */
@Repository
public interface CarDao extends MongoRepository<Car, Integer> {
}
 

package com.sise.docker.dao;

import com.sise.docker.domain.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;

import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * @author idea
 * @data 2019/11/23
 */
@Repository
public class UserDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;


    public void insert() {
        String time = String.valueOf(System.currentTimeMillis());
        String sql = "insert into t_user (username) values ('idea-" + time + "')";
        jdbcTemplate.update(sql);
        System.out.println("==========執行插入語句==========");
    }

    class UserMapper implements RowMapper<User> {

        @Override
        public User mapRow(ResultSet resultSet, int i) throws SQLException {
            User unitPO = new User();
            unitPO.setId(resultSet.getInt("id"));
            unitPO.setUsername(resultSet.getString("username"));
            return unitPO;
        }
    }
}

 

在控制器中添加相關的函數入口:

package com.sise.docker.controller;

import com.sise.docker.dao.CarDao;
import com.sise.docker.dao.UserDao;
import com.sise.docker.domain.Car;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Random;

/**
 * @author idea
 * @data 2019/11/20
 */
@RestController
@RequestMapping(value = "/docker")
public class DockerController {

    @Autowired
    private UserDao userDao;
    @Autowired
    private CarDao carDao;

    @GetMapping(value = "/insert-mongodb")
    public String insertMongoDB() {
        Car car = new Car();
        car.setId(new Random().nextInt(15000000));
        String number = String.valueOf(System.currentTimeMillis());
        car.setNumber(number);
        carDao.save(car);
        return "this is insert-mongodb";
    }

    @GetMapping(value = "/insert-mysql")
    public String insertMySQL() {
        userDao.insert();
        return "this is insert-mysql";
    }

    @GetMapping(value = "/test2")
    public String test() {
        System.out.println("=========docker test222=========");
        return "this is docker test";
    }
}

 

對原先的docker-compose.yml文件添加相應的內容,主要是增加對於mongodb和mysql的依賴模塊,

#docker引擎對應所支持的docker-compose文本格式
version: '3'
services:

  #服務的名稱
  springboot-docker:
    container_name: docker-springboot
    build:
      context: .
      dockerfile: springboot-dockerfile
    ports:
      - "7089:7089"

    depends_on:
      - mongodb


  mongodb:
    #容器的名稱
    container_name: docker-mongodb
    image: daocloud.io/library/mongo:latest
    ports:
      - "27017:27017"

  mysql:
    #鏡像的版本
    image: mysql:5.7
    container_name: docker-mysql
    ports:
      - 3309:3306
    environment:
       MYSQL_DATABASE: test
       MYSQL_ROOT_PASSWORD: root
       MYSQL_ROOT_USER: root
       MYSQL_ROOT_HOST: '%'

 

這裏頭我嘗試將application.yml文件通過不同的profile來進行區分:

應上篇文章中有讀者問到,不同環境不同配置的指定問題,這裡有一種思路,springboot依舊保持原有的按照profile來識別不同環境的配置,具體打包之後讀取的配置,可以通過springboot-dockerfile這份文件的ENTRYPOINT 參數來指定,例如下邊這種格式:

FROM openjdk:8-jdk-alpine

VOLUME /tmp

ADD target/springboot-docker.jar springboot-docker.jar

#設置鏡像的時區,避免出現8小時的誤差
ENV TZ=Asia/Shanghai

EXPOSE 7089
#這裏可以通過-D參數在對jar打包運行的時候指定需要讀取的配置問題
ENTRYPOINT ["java","-Xms256m","-Xmx512m","-Dspring.profiles.active=prod","-jar","springboot-docker.jar"]

 

最後便是我們的yml配置文件內容,由於配置類docker容器的依賴,所以這裏面對於yml的寫法不再是通過ip來訪問相應的數據庫了,而是需要通過service-name的映射來達成目標。

application-prod.yml

server:
  port: 7089

spring:
    data:
      mongodb:
        uri: mongodb://mongodb:27017
        database: test

    datasource:
             driver-class-name: com.mysql.jdbc.Driver
             url: jdbc:mysql://mysql:3306/test?useUnicode=true&amp;characterEncoding=UTF-8
             username: root
             password: root

 

當相關的代碼和文件都整理好了之後,將這份代碼發送到服務器上進行打包。

mvn package

 

接着我們便可以進行docker-compose的啟動了。

這裡有個小坑需要注意一下,由於之前我們已經對單獨的springboot容器進行過打包了,所以在執行docker-compose up指令的時候會優先使用已有的容器,而不是重新創建容器。

這個時候需要先將原先的image鏡像進行手動刪除,再打包操作:

[root@izwz9ic9ggky8kub9x1ptuz springboot-docker]# docker images
REPOSITORY                                           TAG                 IMAGE ID            CREATED             SIZE
springboot-docker                  latest              86f32bd9257f        4 hours ago         128MB
<none>                                               <none>              411616c3d7f7        2 days ago          679MB
<none>                                               <none>              77044e3ad9c2        2 days ago          679MB
<none>                                               <none>              5d9328dd1aca        2 days ago          679MB
springbootmongodocker_springappserver                latest              36237acf08e1        3 days ago          695MB

 

刪除鏡像的命令:

docker rmi 【鏡像id】

 

此時再重新進行docker-compose指令的打包操作即可:

 

docker-compose up

 

啟動之後,可以通過docker-compose自帶的一些指令來進行操作,常用的一些指令我都歸納在了下邊:

 

docker-compose [Command]

Commands:
  build              構建或重建服務
  bundle             從compose配置文件中產生一個docker綁定
  config             驗證並查看compose配置文件
  create             創建服務
  down               停止並移除容器、網絡、鏡像和數據卷
  events             從容器中接收實時的事件
  exec               在一個運行中的容器上執行一個命令
  help               獲取命令的幫助信息
  images             列出所有鏡像
  kill               通過發送SIGKILL信號來停止指定服務的容器
  logs               從容器中查看服務日誌輸出
  pause              暫停服務
  port               打印綁定的公共端口
  ps                 列出所有運行中的容器
  pull               拉取並下載指定服務鏡像
  push               Push service images
  restart            重啟YAML文件中定義的服務
  rm                 刪除指定已經停止服務的容器
  run                在一個服務上執行一條命令
  scale              設置指定服務運行容器的個數
  start              在容器中啟動指定服務
  stop               停止已運行的服務
  top                显示各個服務容器內運行的進程
  unpause            恢復容器服務
  up                 創建並啟動容器
  version            显示Docker-Compose版本信息

 

最後對相應的接口做檢測:

 

相關的完整代碼我已經上傳到了gitee地址,如果有需要的朋友可以前往進行下載。

代碼地址:https://gitee.com/IdeaHome_admin/wfw

 

實踐完畢之後,你可能會覺得有了docker-compose之後,對於多個docker容器來進行管理顯得就特別輕鬆了。

 

但是往往現實中並沒有這麼簡單,docker-compose存在着一個弊端,那就是不能做跨機器之間的docker容器進行管理

 

因此隨者技術的發展,後邊也慢慢出現了一種叫做Kubernetes的技術。Kubernetes(俗稱k8s)是一個開源的,用於管理雲平台中多個主機上的容器化的應用,Kubernetes的目標是讓部署容器化的應用簡單並且高效(powerful),Kubernetes提供了應用部署,規劃,更新,維護的一種機制。

 

Kubernetes這類技術對於小白來說入門的難度較高,後邊可能會抽空專門來寫一篇適合小白閱讀的k8s入門文章。

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

高德服務單元化方案和架構實踐

導讀:本文主要介紹了高德在服務單元化建設方面的一些實踐經驗,服務單元化建設面臨很多共性問題,如請求路由、單元封閉、數據同步,有的有成熟方案可以借鑒和使用,但不同公司的業務不盡相同,要盡可能的結合業務特點,做相應的設計和處理。

一、為什麼要做單元化

  • 單機房資源瓶頸

隨着業務體量和服務用戶群體的增長,單機房或同城雙機房無法支持服務的持續擴容。

  • 服務異地容災

異地容災已經成為核心服務的標配,有的服務雖然進行了多地多機房部署,但數據還是只在中心機房,實現真正意義上的異地多活,就需要對服務進行單元化改造。

二、高德單元化的特點

在做高德單元化項目時,我們首先要考慮的是結合高德的業務特點,看高德的單元化有什麼不一樣的訴求,這樣就清楚哪些經驗和方案是可以直接拿來用的,哪些又是需要我們去解決的。

高德業務和傳統的在線交易業務還是不太一樣,高德為用戶提供以導航為代表的出行服務,很多業務場景對服務的RT要求會很高,所以在做單元化方案時,盡可能減少對整體服務RT的影響就是我們需要重點考慮的問題,盡量做到數據離用戶近一些。轉換到單元化技術層面需要解決兩個問題:

1.用戶設備的單元接入需要盡可能的做到就近接入,用戶真實地理位置接近哪個單元就接入哪個單元,如華北用戶接入到張北,華南接入到深圳。

2.用戶的單元劃分最好能與就近接入的單元保持一致,減少單元間的跨單元路由。如用戶請求從深圳進來,用戶的單元劃分最好就在深圳單元,如果劃到張北單元就會造成跨單元路由。

另外一個區別就是高德很多業務是無須登錄的,所以我們的單元化方案除了用戶ID也要支持基於設備ID。

三、高德單元化實踐

服務的單元化架構改造需要一個至上而下的系統性設計,核心要解決請求路由、單元封閉、數據同步三方面問題。

請求路由:根據高德業務的特點,我們提供了取模路由和路由表路由兩種策略,目前上線應用使用較多的是路由表路由策略。

單元封閉:得益於集團的基礎設施建設,我們使用vipserver、hsf等服務治理能力保證服務同機房調用,從而實現單元封閉(hsf unit模式也是一種可行的方案,但個人認為同機房調用的架構和模式更簡潔且易於維護)。

數據同步:數據部分使用的是集團DB產品提供的DRC數據同步。

單元路由服務採用什麼樣的部署方案是我們另一個要面臨的問題,考慮過以下三種方案:

第一種SDK的方式因為對業務的強侵入性是首先被排除的,統一接入層進行代理和去中心化插件集成兩種方案各有利弊,但當時首批要接入單元化架構的服務很多都還沒有統一接入到gateway,所以基於現狀的考慮使用了去中心化插件集成的方式,通過在應用的nginx集成UnitRouter。

服務單元化架構

目前高德賬號,雲同步、用戶評論系統都完成了單元化改造,採用三地四機房部署,寫入量較高的雲同步服務,單元寫高峰能達到數w+QPS (存儲是mongodb集群)。

以賬號系統為例介紹下高德單元化應用的整體架構。

賬號系統服務是三地四機房部署,數據分別存儲在tair為代表的緩存和XDB里,數據存儲三地集群部署、全量同步。賬號系統服務器的Tengine上安裝UntiRouter,它請求的負責單元識別和路由,用戶單元劃分是通過記錄用戶與單元關係的路由表來控制。

PS:因歷史原因緩存使用了tair和自建的uredis(在redis基礎上添加了基於log的數據同步功能),目前已經在逐步統一到tair。數據同步依賴tair和alisql的數據同步方案,以及自建的uredis數據同步能力。

就近接入實現方案

為滿足高德業務低延時要求,就要想辦法做到數據(單元)離用戶更近,其中有兩個關鍵鏈路,一個是通過aserver接入的外網連接,另一個是服務內部路由(盡可能不產生跨單元路由)。

措施1:客戶端的外網接入通過aserver上的配置,將不同地理區域(七個大區)的設備劃分到對應近的單元,如華北用戶接入張北單元。

措施2:通過記錄用戶和單元關係的路由表來劃分用戶所屬單元,這個關係是通過系統日誌分析出來的,用戶經常從哪個單元入口進來,就會把用戶劃分到哪個單元,從而保證請求入口和單元劃分的相對一致,從而減少跨單元路由。

所以,在最終的單元路由實現上我們提供了傳統的取模路由,和為降延時而設計的基於路由表路由兩種策略。同時,為解無須登錄的業務場景問題,上述兩種策略除了支持用戶ID,我們同時也支持設備ID。

路由表設計

路由表分為兩部分,一個是用戶-分組的關係映射表,另一個是分組-單元的關係映射表。在使用時,通過路由表查對應的分組,再通過分組看用戶所屬單元。分組對應中國大陸的七個大區。

先看“用戶-(大區)分組”:

路由表是定期通過系統日誌分析出來的,看用戶最近IP屬於哪個大區就劃分進哪個分組,同時也對應上了具體單元。當一個北京的用戶長期去了深圳,因IP的變化路由表更新后將划進新大區分組,從而完成用戶從張北單元到深圳單元的遷移。

再看“分組-單元”:

分組與單元的映射有一個默認關係,這是按地理就近來配置的,比如華南對應深圳。除了默認的映射關係,還有幾個用於切流預案的關係映射。

老用戶可以通過路由表來查找單元,新用戶怎麼辦?對於新用戶的處理我們會降級成取模的策略進行單元路由,直至下次路由表的更新。所以整體上看新用戶跨單元路由比例肯定是比老用戶大的多,但因為新用戶是一個相對穩定的增量,所以整體比例在可接受範圍內。

路由計算

有了路由表,接下來就要解工程化應用的問題,性能、空間、靈活性和準確率,以及對服務穩定性的影響這幾個方面是要進行綜合考慮的,首先考慮外部存儲會增加服務的穩定性風險,後面我們在BloomFilter 、BitMap和MapDB多種方案中選擇BloomFilter,萬分之幾的誤命中率導致的跨單元路由在業務可接受範圍內。

通過日誌分析出用戶所屬大區后,我們將不同分組做成多個布隆過濾器,計算時逐層過濾。這個計算有兩種特殊情況:

1) 因為BloomFilter存在誤算率,有可能存在一種情況,華南分組的用戶被計算到華北了,這種情況比例在萬分之3 (生成BloomFilter時可調整),它對業務上沒有什麼影響,這類用戶相當於被劃分到一個非所在大區的分組裡,但這個關係是穩定的,不會影響到業務,只是存在跨單元路由,是可接受的。

2) 新用戶不在分組信息里,所以經過逐層的計算也沒有匹配到對應大區分組,此時會使用取模進行模除分組的計算。

如果業務使用的是取模路由而非路由表路由策略,則直接根據tid或uid計算對應的模除分組,原理簡單不詳表了。

單元切流

在發生單元故障進行切流時,主要分為四步驟

打開單元禁寫 (跨單元寫不敏感業務可以不配置)

檢查業務延時

切換預案

解除單元禁寫

PS:更新路由表時,也需要上述操作,只是第3步的切換預案變成切換新版本路由表;單元禁寫主要是了等待數據同步,避免數據不一致導致的業務問題。

核心指標

單元計算耗時1~2ms

跨單元路由比例底於5%

除了性能外,因就近接入的訴求,跨單元路由比例也是我們比較關心的重要指標。從線上觀察看,路由表策略單元計算基本上在1、2ms內完成,跨單元路由比例3%左右,整體底於5%。

四、後續優化

統一接入集成單元化能力

目前大部分服務都接入了統一接入網關服務,在網關集成單元化能力將大大減少服務單元化部署的成本,通過簡單的配置就可以實現單元路由,服務可將更多的精力放在業務的單元封閉和數據同步上。

分組機制的優化

按大區分組存在三個問題:

通過IP計算大區有一定的誤算率,會導致部分用戶劃分錯誤分組。

分組粒度太大,單元切流時流量不好分配。舉例,假如華東是我們用戶集中的大區,切流時把這個分組切到任意一個指定單元,都會造成單元服務壓力過大。

計算次數多,分多少個大區,理論最大計算次數是有多少次,最後採取取模策略。

針對上述幾個問題我們計劃對分組機製做如下改進

通過用戶進入單元的記錄來確認用戶所屬單元,而非根據用戶IP所在大區來判斷,解上述問題1。

每個單元劃分4個虛擬分組,支持更細粒度單元切流,解上述問題2。

用戶確實單元后,通過取模來劃分到不同的虛擬分組。每個單元只要一次計算就能完成,新用戶只需經過3次計算,解上述問題3。

熱更時的雙表計算

與取模路由策略不同,路由表策略為了把跨單元路由控制在一個較好的水平需要定期更新,目前更新時需要一個短暫的單元禁寫,這對於很多業務來說是不太能接受的。

為優化這個問題,系統將在路由表更新時做雙(路由)表計算,即將新老路由表同時加載進內存,更新時不再對業務做完全的禁寫,我們會分別計算當前用戶(或設備)在新老路由表的單元結果,如果單元一致,則說明路由表的更新沒有導致該用戶(或設備)變更單元,所以請求會被放行,相反如果計算結果是不同單元,說明發生了單元變更,該請求會被攔截,直至到達新路由表的一個完全起用時間。

優化前服務會完全禁寫比如10秒(時間取決於數據同步時間),優化後會變成觸髮禁寫的是這10秒內路由發生變更的用戶,這將大大減少對業務的影響。

服務端數據驅動的單元化場景

前面提到高德在路由策略上結合業務的特別設計,但整體單元劃分還是以用戶(或設備)為維度來進行的,但高德業務還有一個大的場景是我們未來要面對和解決的,就是以數據維度驅動的單元設計,基於終端的服務路由會變成基於數據域的服務路由。

高德很多服務是以服務數據為核心的,像地圖數據等它並非由用戶直接產生。業務的發展數據存儲也將不斷增加,包括5G和自動駕駛,對應數據的爆髮式增長單點全量存儲並不實現,以服務端數據驅動的服務單元化設計,是我們接下來要考慮的重要應用場景。

寫在最後

不同的業務場景對單元化會有不同的訴求,我們提供不同的策略和能力供業務進行選擇,對於多數據服務我們建議使用業務取模路由,簡單且易於維護;對於RT敏感的服務使用路由表的策略來盡可能的降低服務響應時長的影響。另外,要注意的是強依賴性的服務要採用相同的路由策略。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!