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