Spring Boot 微信點(diǎn)餐開源系統(tǒng)!

來源 | jianshu.com/p/ae14101989f2
-
架構(gòu) -
分布式鎖 -
分布式鎖基于Redis的實(shí)現(xiàn):(本系統(tǒng)鎖才用的) -
分布式鎖基于Zookeeper的實(shí)現(xiàn) -
Zookeeper的分布式鎖原理 -
分布式系統(tǒng)的下的Session -
Redis作為分布式鎖 -
其它
架構(gòu)
前后端分離:
Nginx與Tomcat的關(guān)系在這篇文章,幾分鐘可以快速了解:
https://www.jianshu.com/p/22dcb7ef9172
補(bǔ)充:
-
setting.xml 文件的作用:settings.xml是maven的全局配置文件。 而pom.xml文件是所在項(xiàng)目的局部配置。Settings.xml中包含類似本地倉儲(chǔ)位置、修改遠(yuǎn)程倉儲(chǔ)服務(wù)器、認(rèn)證信息等配置。 -
maven的作用:借助Maven,可將jar包僅僅保存在“倉庫”中 ,有需要該文件時(shí),就引用該文件接口,不需要復(fù)制文件過來占用空間 。
注:這個(gè)“倉庫”應(yīng)該就是本地安裝maven的目錄下的Repository的文件夾
分布式鎖
線程鎖: 當(dāng)某個(gè)方法或代碼使用鎖,在同一時(shí)刻僅有一個(gè)線程執(zhí)行該方法或該代碼段。線程鎖只在同一JVM中有效,因?yàn)榫€程鎖的實(shí)現(xiàn)在根本上是依靠線程之間共享內(nèi)存實(shí)現(xiàn)的。如synchronized
進(jìn)程鎖: 為了控制同一操作系統(tǒng)中多個(gè)進(jìn)程訪問某個(gè)共享資源。
分布式鎖: 當(dāng)多個(gè)進(jìn)程不在同一個(gè)系統(tǒng)中,用分布式鎖控制多個(gè)進(jìn)程對(duì)資源的訪問。
分布式鎖一般有三種實(shí)現(xiàn)方式:
-
數(shù)據(jù)庫樂觀鎖; -
基于Redis的分布式鎖; -
基于ZooKeeper的分布式鎖。
樂觀鎖的實(shí)現(xiàn): 使用版本標(biāo)識(shí)來確定讀到的數(shù)據(jù)與提交時(shí)的數(shù)據(jù)是否一致。提交后修改版本標(biāo)識(shí),不一致時(shí)可以采取丟棄和再次嘗試的策略。
CAS: 可以閱讀這篇文章:
https://www.jianshu.com/p/456bb1ea9627
分布式鎖基于Redis的實(shí)現(xiàn):(本系統(tǒng)鎖才用的)
基本命令:
-
SETNX(SET if Not exist):當(dāng)且僅當(dāng) key 不存在,將 key 的值設(shè)為 value ,并返回1;若給定的 key 已經(jīng)存在,則 SETNX 不做任何動(dòng)作,并返回0。 -
GETSET:將給定 key 的值設(shè)為 value ,并返回 key 的舊值。先根據(jù)key獲取到舊的value,再set新的value。 -
EXPIRE 為給定 key 設(shè)置生存時(shí)間,當(dāng) key 過期時(shí),它會(huì)被自動(dòng)刪除。
加鎖方式:
這里的jedis是Java對(duì)Redis的集成
jedis.set(String key, String value, String nxxx, String expx, int time)
錯(cuò)誤的加鎖方式1:
如果程序在執(zhí)行完setnx()之后突然崩潰,導(dǎo)致鎖沒有設(shè)置過期時(shí)間。那么將會(huì)發(fā)生死鎖。
Long result = jedis.setnx(Key, value);
if (result == 1) {
// 若在這里程序突然崩潰,則無法設(shè)置過期時(shí)間,將發(fā)生死鎖
jedis.expire(Key, expireTime);
}
錯(cuò)誤的加鎖方式2:
分布式鎖才用(Key,過期時(shí)間)的方式,如果鎖存在,那么獲取它的過期時(shí)間,如果鎖的確已經(jīng)過期了,那么獲得鎖,并且設(shè)置新的過期時(shí)間
錯(cuò)誤分析:不同的客戶端之間需要同步好時(shí)間。
long expires = System.currentTimeMillis() + expireTime;
String expiresStr = String.valueOf(expires);
// 如果當(dāng)前鎖不存在,返回加鎖成功
if (jedis.setnx(lockKey, expiresStr) == 1) {
return true;
}
// 如果鎖存在,獲取鎖的過期時(shí)間
String currentValueStr = jedis.get(lockKey);
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
// 鎖已過期,獲取上一個(gè)鎖的過期時(shí)間,并設(shè)置現(xiàn)在鎖的過期時(shí)間
String oldValueStr = jedis.getSet(lockKey, expiresStr);
if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
// 考慮多線程并發(fā)的情況,只有一個(gè)線程的設(shè)置值和當(dāng)前值相同,它才有權(quán)利加鎖
return true;
}
}
// 其他情況,一律返回加鎖失敗
return false;
解鎖:判斷鎖的擁有者后可以使用 jedis.del(lockKey) 來釋放鎖。
分布式鎖基于Zookeeper的實(shí)現(xiàn)
Zookeeper簡介: Zookeeper提供一個(gè)多層級(jí)的節(jié)點(diǎn)命名空間(節(jié)點(diǎn)稱為znode),每個(gè)節(jié)點(diǎn)都用一個(gè)以斜杠(/)分隔的路徑表示,而且每個(gè)節(jié)點(diǎn)都有父節(jié)點(diǎn)(根節(jié)點(diǎn)除外)。
例如,/foo/doo這個(gè)表示一個(gè)znode,它的父節(jié)點(diǎn)為/foo,父父節(jié)點(diǎn)為/,而/為根節(jié)點(diǎn)沒有父節(jié)點(diǎn)。
client不論連接到哪個(gè)Server,展示給它都是同一個(gè)視圖,這是zookeeper最重要的性能。
Zookeeper 的核心是原子廣播,這個(gè)機(jī)制保證了各個(gè)Server之間的同步。實(shí)現(xiàn)這個(gè)機(jī)制的協(xié)議叫做Zab協(xié)議。Zab協(xié)議有兩種模式,它們分別是恢復(fù)模式(選主)和廣播模式(同步)。 當(dāng)服務(wù)啟動(dòng)或者在領(lǐng)導(dǎo)者崩潰后,Zab就進(jìn)入了恢復(fù)模式 ,當(dāng)領(lǐng)導(dǎo)者被選舉出來,且大多數(shù)Server完成了和 leader的狀態(tài)同步以后,恢復(fù)模式就結(jié)束了。狀態(tài)同步保證了leader和Server具有相同的系統(tǒng)狀態(tài)。
為了保證事務(wù)的順序一致性,zookeeper采用了遞增的事務(wù)id號(hào)(zxid)來標(biāo)識(shí)事務(wù),實(shí)現(xiàn)中zxid是一個(gè)64位的數(shù)字。
Zookeeper的分布式鎖原理
獲取分布式鎖的流程:
-
在獲取分布式鎖的時(shí)候在locker節(jié)點(diǎn)(locker節(jié)點(diǎn)是Zookeeper的指定節(jié)點(diǎn))下創(chuàng)建臨時(shí)順序節(jié)點(diǎn),釋放鎖的時(shí)候刪除該臨時(shí)節(jié)點(diǎn)。 -
客戶端調(diào)用createNode方法在locker下創(chuàng)建臨時(shí)順序節(jié)點(diǎn),然后調(diào)用getChildren(“l(fā)ocker”)來獲取locker下面的所有子節(jié)點(diǎn),注意此時(shí)不用設(shè)置任何Watcher。 -
客戶端獲取到所有的子節(jié)點(diǎn)path之后,如果發(fā)現(xiàn)自己創(chuàng)建的子節(jié)點(diǎn)序號(hào)最小,那么就認(rèn)為該客戶端獲取到了鎖。 -
如果發(fā)現(xiàn)自己創(chuàng)建的節(jié)點(diǎn)并非locker所有子節(jié)點(diǎn)中最小的,說明自己還沒有獲取到鎖,此時(shí)客戶端需要找到比自己小的那個(gè)節(jié)點(diǎn),然后對(duì)其調(diào)用exist()方法,同時(shí)對(duì)其注冊(cè)事件監(jiān)聽器。 -
之后,讓這個(gè)被關(guān)注的節(jié)點(diǎn)刪除,則客戶端的Watcher會(huì)收到相應(yīng)通知,此時(shí)再次判斷自己創(chuàng)建的節(jié)點(diǎn)是否是locker子節(jié)點(diǎn)中序號(hào)最小的,如果是則獲取到了鎖,如果不是則重復(fù)以上步驟繼續(xù)獲取到比自己小的一個(gè)節(jié)點(diǎn)并注冊(cè)監(jiān)聽。
我的解釋:
A在Locker下創(chuàng)建了Node_n —>循環(huán) ( 每次獲取Locker下的所有子節(jié)點(diǎn) —> 對(duì)這些節(jié)點(diǎn)按節(jié)點(diǎn)自增號(hào)排序順序 —> 判斷自己創(chuàng)建的Node_n是否是第一個(gè)節(jié)點(diǎn) —> 如果是則獲得了分布式鎖 —> 如果不是監(jiān)聽上一個(gè)節(jié)點(diǎn)Node_n-1 等它釋放掉分布式鎖。)
“
@ControllerAdvice處理全局異常 Mybatis注解方式的使用:@insert 用注解方式寫SQL語句
分布式系統(tǒng)的下的Session
1、分布式系統(tǒng): 多節(jié)點(diǎn),節(jié)點(diǎn)發(fā)送數(shù)據(jù)交互,不共享主內(nèi)存,但通過網(wǎng)絡(luò)發(fā)送消息合作。
分布式:不同功能模塊的節(jié)點(diǎn)
集群:相同功能的節(jié)點(diǎn)
2、Session 與token
服務(wù)端在HTTP頭里設(shè)置SessionID而客戶端將其保存在cookie
而使用Token時(shí)需要手動(dòng)在HTTP頭里設(shè)置,服務(wù)器收到請(qǐng)求后取出cookie進(jìn)行驗(yàn)證。
都是一個(gè)用戶一個(gè)標(biāo)志
3、分布式系統(tǒng)中的Session問題:
高并發(fā):通過設(shè)計(jì)保證系統(tǒng)能夠同時(shí)并行處理很多請(qǐng)求。
當(dāng)高并發(fā)量的請(qǐng)求到達(dá)服務(wù)端的時(shí)候通過負(fù)載均衡的方式分發(fā)到集群中的某個(gè)服務(wù)器,這樣就有可能導(dǎo)致同一個(gè)用戶的多次請(qǐng)求被分發(fā)到集群的不同服務(wù)器上,就會(huì)出現(xiàn)取不到session數(shù)據(jù)的情況。
根據(jù)訪問不同的URL,負(fù)載到不同的服務(wù)器上去
三臺(tái)機(jī)器,A1部署類目,A2部署商品,A3部署單服務(wù)
通用方案:用Redis保存Session信息,服務(wù)器需要時(shí)都去找Redis要。登錄時(shí)保存好key-value,登出時(shí)讓他失效
垂直擴(kuò)展:IP哈希 IP的哈希值相同的訪問同一臺(tái)服務(wù)器
session的一致性:只要用戶不重啟瀏覽器,每次http短連接請(qǐng)求,理論上服務(wù)端都能定位到session,保持會(huì)話。
Redis作為分布式鎖
高并發(fā):通過設(shè)計(jì)保證系統(tǒng)能夠同時(shí)并行處理很多請(qǐng)求。(系統(tǒng)學(xué)習(xí)并發(fā)知識(shí),可以在Java知音公眾號(hào)回復(fù)“多線程聚合”)
同步:Java中的同步指的是通過人為的控制和調(diào)度,保證共享資源的多線程訪問成為線程安全。
線程的Block狀態(tài):
a.調(diào)用join()和sleep()方法,sleep()時(shí)間結(jié)束或被打斷
b.wait(),使該線程處于等待池,直到notify()/notifyAll():不釋放資源
此外,在runnable狀態(tài)的線程是處于被調(diào)度的線程,Thread類中的yield方法可以讓一個(gè)running狀態(tài)的線程轉(zhuǎn)入runnable。
Q:為什么wait,notify和notifyAll必須與synchronized一起使用?Obj.wait()、Obj.notify必須在synchronized(Obj){…}語句塊內(nèi)。
A:wait就是說線程在獲取對(duì)象鎖后,主動(dòng)釋放對(duì)象鎖,同時(shí)本線程休眠。
Q:Synchronized:
A:Synchronized就是非公平鎖,它無法保證等待的線程獲取鎖的順序。
公平和非公平鎖的隊(duì)列都基于鎖內(nèi)部維護(hù)的一個(gè)雙向鏈表,表結(jié)點(diǎn)Node的值就是每一個(gè)請(qǐng)求當(dāng)前鎖的線程。公平鎖則在于每次都是依次從隊(duì)首取值。
ReentrantLock重入性:
重入鎖可以看這兩篇文章,都比較簡單
https://www.jianshu.com/p/587a4559442b https://www.jianshu.com/p/1c52f17efaab
Spring + Redis緩存的兩個(gè)重要注解:
-
@cacheable 只會(huì)執(zhí)行一次,當(dāng)標(biāo)記在一個(gè)方法上時(shí)表示該方法是支持緩存的,Spring會(huì)在其被調(diào)用后將其返回值緩存起來,以保證下次利用同樣的參數(shù)來執(zhí)行該方法時(shí)可以直接從緩存中獲取結(jié)果。 -
@cacheput:與@Cacheable不同的是使用@CachePut標(biāo)注的方法在執(zhí)行前不會(huì)去檢查緩存中是否存在之前執(zhí)行過的結(jié)果,而是每次都會(huì)執(zhí)行該方法,并將執(zhí)行結(jié)果以鍵值對(duì)的形式存入指定的緩存中。
對(duì)數(shù)據(jù)庫加鎖(樂觀鎖 與 悲觀鎖)
悲觀鎖依賴數(shù)據(jù)庫實(shí)現(xiàn):
select * from account where name=”Erica” for update
這條sql 語句鎖定了account 表中所有符合檢索條件(name=”Erica”)的記錄,使該記錄在修改期間其它線程不得占有。
代碼層加鎖:
String hql ="from TUser as user where user.name='Erica'";
Query query = session.createQuery(hql);
query.setLockMode("user",LockMode.UPGRADE); //加鎖
List userList = query.list();//執(zhí)行查詢,獲取數(shù)據(jù)
其它
@Data 類似于自動(dòng)生成了Getter()、Setter()、ToString()等方法。
JAVA1.8的新特性StreamAPI:Collectors中提供了將流中的元素累積到匯聚結(jié)果的各種方式
List
For - each 寫法:
for each語句是java5新增,在遍歷數(shù)組、集合的時(shí)候,for each擁有不錯(cuò)的性能。
public static void main(String[] args) {
String[] names = {"beibei", "jingjing"};
for (String name : names) {
System.out.println(name);
}
}
for each雖然能遍歷數(shù)組或者集合,但是只能用來遍歷,無法在遍歷的過程中對(duì)數(shù)組或者集合進(jìn)行修改。
BindingResult:一個(gè)@Valid的參數(shù)后必須緊挨著一個(gè)BindingResult 參數(shù) ,否則spring會(huì)在校驗(yàn)不通過時(shí)直接拋出異常。
@Data
public class OrderForm {
@NotEmpty(message = "姓名必填")
private String name;
}
后臺(tái):
@RequestMapping("save")
public String save( @Valid OrderForm order,BindingResult result) {
//
if(result.hasErrors()){
List
ls=result.getAllErrors();
for (
int i =
0; i < ls.size(); i++) {
log.error(
"參數(shù)不正確,OrderForm={}", order);
throw
new SellException(
………… ,
result.getFeildError.getDefaultMessage()
)
System.out.println(
"error:"+ls.get(i));
}
}
return
"adduser";
}
result.getFeildError.getDefaultMessage()可拋出“姓名必填” 的異常。
4、List轉(zhuǎn)為Map
public class Apple {
private Integer id;
private String name;
private BigDecimal money;
private Integer num;
/*構(gòu)造函數(shù)*/
}
List
appleList =
new ArrayList<>();
//存放apple對(duì)象集合
Apple apple1 =
new Apple(
1,
"蘋果1",
new BigDecimal(
"3.25"),
10);
Apple apple12 =
new Apple(
1,
"蘋果2",
new BigDecimal(
"1.35"),
20);
Apple apple2 =
new Apple(
2,
"香蕉",
new BigDecimal(
"2.89"),
30);
Apple apple3 =
new Apple(
3,
"荔枝",
new BigDecimal(
"9.99"),
40);
appleList.add(apple1);
appleList.add(apple12);
appleList.add(apple2);
appleList.add(apple3);
Map
appleMap =
appleList.stream().collect(Collectors.toMap(Apple::getId, a -> a,(k1,k2)->k1));
5、Collection的子類:List、Set

List:ArrayList、LinkedList 、Vector
List:有序容器,允許null元素,允許重復(fù)元素
Set:元素是無序的,不允許元素
最流行的是基于 HashMap 實(shí)現(xiàn)的 HashSet,由hashCode()和equals()保證元素的唯一性。
可以用set幫助去掉List中的重復(fù)元素,set的構(gòu)造方法的參數(shù)可以是List,構(gòu)造后是一個(gè)去重的set。
HashMap的補(bǔ)充:它不是Collection下的
Map可以使用containsKey()/containsValue()來檢查其中是否含有某個(gè)key/value。
HashMap會(huì)利用對(duì)象的hashCode來快速找到key。
插入過程:通過一個(gè)hash函數(shù)確定Entry的插入位置index=hash(key),但是數(shù)組的長度有限,可能會(huì)發(fā)生index沖突,當(dāng)發(fā)生了沖突時(shí),會(huì)使用頭插法,即為新來的Entry指向舊的Entry,成為一個(gè)鏈表。
每次插入時(shí)依次遍歷它的index下的單鏈表,如果存在Key一致的節(jié)點(diǎn),那么直接替換,并且返回新的值。
但是單鏈表不會(huì)一直增加元素,當(dāng)元素個(gè)數(shù)超過8個(gè)時(shí),會(huì)嘗試將單鏈表轉(zhuǎn)化為紅黑樹存儲(chǔ)。
為何加載因子默認(rèn)為0.75? (0.75開始擴(kuò)容)
答:通過源碼里的javadoc注釋看到,元素在哈希表中分布的桶頻率服從參數(shù)為0.5的泊松分布。
源碼地址:
https://github.com/923310233/wxOrder
END

有熱門推薦?
1. 你還在認(rèn)為 count(1) 比 count(*) 效率高?
2. 收到面試通知后,如下的準(zhǔn)備可以大大提升面試成功率?
最近面試BAT,整理一份面試資料《Java面試BATJ通關(guān)手冊(cè)》,覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫、數(shù)據(jù)結(jié)構(gòu)等等。
獲取方式:點(diǎn)“在看”,關(guān)注公眾號(hào)并回復(fù) Java 領(lǐng)取,更多內(nèi)容陸續(xù)奉上。
文章有幫助的話,在看,轉(zhuǎn)發(fā)吧。
謝謝支持喲 (*^__^*)
