實戰 | SpringBoot微信點餐系統(附源碼)

架構

前後端分離:

補充:

  • setting.xml 文件的作用:settings.xml是maven的全局配置文件。而pom.xml文件是所在項目的局部配置。Settings.xml中包含類似本地倉儲位置、修改遠程倉儲服務器、認證信息等配置。

  • maven的作用:藉助Maven,可將jar包僅僅保存在“倉庫”中,有需要該文件時,就引用該文件接口,不需要複製文件過來佔用空間

注:這個“倉庫”應該就是本地安裝maven的目錄下的Repository的文件夾

分佈式鎖

線程鎖:當某個方法或代碼使用鎖,在同一時刻僅有一個線程執行該方法或該代碼段。線程鎖只在同一JVM中有效,因爲線程鎖的實現在根本上是依靠線程之間共享內存實現的。如synchronized

進程鎖:爲了控制同一操作系統中多個進程訪問某個共享資源。

分佈式鎖:當多個進程不在同一個系統中,用分佈式鎖控制多個進程對資源的訪問。

分佈式鎖一般有三種實現方式:

  1. 數據庫樂觀鎖;

  2. 基於Redis的分佈式鎖;

  3. 基於ZooKeeper的分佈式鎖。

樂觀鎖的實現:使用版本標識來確定讀到的數據與提交時的數據是否一致。提交後修改版本標識,不一致時可以採取丟棄和再次嘗試的策略。

分佈式鎖基於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的分佈式鎖原理

獲取分佈式鎖的流程:

  1. 在獲取分佈式鎖的時候在locker節點(locker節點是Zookeeper的指定節點)下創建臨時順序節點,釋放鎖的時候刪除該臨時節點。

  2. 客戶端調用createNode方法在locker下創建臨時順序節點,然後調用getChildren(“locker”)來獲取locker下面的所有子節點,注意此時不用設置任何Watcher。

  3. 客戶端獲取到所有的子節點path之後,如果發現自己創建的子節點序號最小,那麼就認爲該客戶端獲取到了鎖。

  4. 如果發現自己創建的節點並非locker所有子節點中最小的,說明自己還沒有獲取到鎖,此時客戶端需要找到比自己小的那個節點,然後對其調用exist()方法,同時對其註冊事件監聽器。

  5. 之後,讓這個被關注的節點刪除,則客戶端的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知音公衆號回覆“多線程聚合”)

同步: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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章