Java 高併發項目筆記

maven 配置與安裝css

下載maven 文件
解壓以後,將文件移動到\Applications文件夾內html

使用cd ~ 進入根目錄文件夾 用ls -a列出文件夾內的全部文件,找到一個名稱爲.bash_profile的文件,打開該文件來配置環境變量前端

export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-10.0.2.jdk/Contents/Home
export MAVEN_HOME=/Applications/apache-maven-3.5.4
export PATH=$PATH:$MAVEN_HOME/bin

配置完以後 添加/調整
商家 -> 庫存
<-
發貨/核帳source .bash_profile編譯一下 而後在命令行之中用mvn - v來檢查一下是否安裝成功java

配置環境變量
注意默認的cd ~可能不是根目錄而是當前用戶的目錄 User\auguskong\mysql

mvn archetype:generate -DgroupId=org.seckill -DartifactId=seckill -DarchetypeArtifactId=maven-archetype-webappweb

修改servlet版本爲3.1redis

秒殺系統業務分析

添加(+)/調整(+/-)

商家 -> 庫存
<-
發貨/核帳spring

付款(-)/退貨(+)

用戶 -> 庫存
<-
秒殺/預售sql

秒殺業務的核心在於庫存的處理數據庫

用戶:

【減庫存 + 記錄購買明細 】
|
【完整事務】 誰 + 時間/有效期 + 付款/發貨信息
|
【數據落地】

爲何須要事務? -> 事務機制依然是最可靠的數據落地方案
考慮一下情景:
減庫存沒有記錄購買明細?
記錄了購買明細可是沒有減庫存?
出現超賣/少買?

秒殺系統難點分析 -> 競爭
同一時間多個用戶購買

事務 + 行級鎖

Start Transaction
Update 庫存數量
Insert 購買明細
Commit

update table set num = num - 1 where id = 10 and num > 1

數據庫設計與編碼

數據庫路徑設置
export PATH=$PATH:/usr/local/mysql/bin/

BUG!! 中英文輸入法的問號意義不同 必須使用英文的問號!!

使用 mysql -u root -p 來打開mysql面板
show create table seckill\G 查看如何建立這個表

手寫DDL
記錄每次上線的DDL修改
--上線V1.1
ALTER TABLE seckill
DROP INDEX idx_create_time,
add index idx_c_s(start_time, create_time);

DAO實體和接口編碼

TABLE -> ENTITY 表對應java類 列對應java類當中的屬性

Command + N 打開generate 可讓編譯器自動生成getter 和 setter 方法

MyBatis 映射

數據庫 -> 映射 -> 對象

爲何能夠過濾重複? 聯合主鍵是??
聯合主鍵裏面有電話號碼,由主鍵不容許重複的特性保證了不存在兩個相同的電話號碼,能夠防止一個用戶重複秒殺

4-3 基於MyBatis實現DAO理論
MyBatis, Hibernate, JDBC 都是作映射的工做 對象關係映射?

MyBatis 特色

須要提供參數 + SQL(SQL必須用戶來寫)

SQL寫在哪?

1.【推薦】XML當中
2.註解提供SQL (註解自己仍是Java源碼 且SQL存在拼接時很麻煩) @SQL Java 1.5以後提供
註解的缺點: 註解本質上也是Java源碼,須要編譯以後才能執行 複雜拼接邏輯會很複雜

如何實現DAO接口?
1.【推薦】Mapper 自動實現DAO接口 這樣能夠只關注於SQL接口,
2.API編程方式實現DAO接口

寫三個SQL

本身控制SQL

MyBatis整合Spring的過程

目的:
使用更少的編碼: 原則是隻寫接口,不寫實現 (實現是MyBatis自動實現)
Seckill queryById(long id)
接口參數
名字表明行爲
接口返回類型表示結果集
更少的配置:
* MyBatis 自動找到包名 減小了包名的代碼
* 自動掃描配置文件 減去配置維護成本

保存足夠的靈活性:本身定製 SQL + 自由傳參

默認

DAO層單元測試編碼和問題排查

import org.junit 文件包不存在的問題

//使用@Param 註解來告知MyBatis 相關參數的名字
List<Seckill> queryAll(@Param("offset") int offet, @Param("limit") int limit);

MyBatis和Spring的整合就在於配置文件的設置

public void insertSuccessKilled() {
        long id = 1001L;
        long phone = 13421234569L;
        int insertCount = successKilledDao.insertSuccessKilled(id, phone);
        System.out.println("insertCount= " + insertCount);
    }

報錯空指針異常
***

命令行命令:

ifconfig能夠查看本地IP地址
MySQL 8.0當中設置密碼的新語法 SET PASSWORD FOR 'jeffrey'@'localhost' = 'auth_string';
按兩次shift快速查找project當中的文件

bug log:

URL is not registered

mapper裏面筆誤 parameterType 改成resultType
mybatis config文件裏面筆誤

肯定mysql的版本是和本機版本相同 進入mysql命令行以後 輸入select version() 或 直接在進入時看初始加載信息

數據庫鏈接失敗可能緣由:

  1. jdbc.properties 配置錯誤:
    1. jdbc.driver: 注意check manual確保命令正確
    2. jdbc.url:
    3. username + password
  2. 未受權遠程訪問或沒有訪問權限 須要經過SQL GRANT進行相關操做
  3. 項目未引入對應版本的驅動jar包,配置文件的設定 mysql-connector-java-5.1.6-bin.jar

由於沒有設定時區而致使了bug, 這個有點扯淡了
jdbc.url=jdbc:mysql://localhost/seckill?useSSL=false&serverTimezone=UTC&useUnicode=true&characterEncoding=utf8

DAO層編碼
編寫接口
邏輯代碼一行沒寫

有什麼好處?

DAO層演變爲接口設計 + SQL編寫
代碼和SQL的分離
DAO拼接等邏輯在Service層完成 邏輯放在Service層當中
對遠程存儲系統作操做的 放在DAO層


秒殺Service接口設計

常量中的數據字典? 使用枚舉來表示常量數據字段??

站在使用者的角度設計接口
配置是一次性工做

不優雅的代碼
return new SeckillExecution(seckillId, 1, "秒殺成功", successKilled);
數據字典使用枚舉進行優化

使用Spring託管Service依賴理論

Spring IOC功能理解

對象工廠: 建立對象的過程 SeckillService
依賴管理:
經過使用Spring 獲得一致性的訪問接口

爲何要用IOC:

  • 對象建立統一託管:再也不使用New關鍵字
  • 規範的生命週期管理 CRUP每一個階段均可以進行添加 更新邏輯 建立好以後,再加入其它方法添加屬性 或修改以前New的方式會致使混亂
  • 靈活的依賴注入:多種方式實現:1.能夠經過註解 2.application context 3.第三方框架自動整合
  • 一致的獲取對象方式: 能夠用容器當中任意的已知對象示例,默認都是單例

Spring-IOC注入方式和使用場景
|XML | 註解 | Java配置類
-----| ----- | ------
舉例| MyBatis整合時 | DAO Service類中加入註解 | Java配置註解
場景| Bean 實現類來自第三方類庫時2.須要命名空間配置,如context,aop,mvc等 | 項目當中自身開發使用的類,能夠直接在代碼中使用註解 @Service@Controller | 不常見經過代碼控制對象建立邏輯: 自定義修改依賴類庫

本項目IOC使用:

  • XML配置
  • package-scan一個機制,MyBatis掃描特定DAO
  • Annotation註解

Service層的依賴注入, 包掃描 + 添加註解@Autowired

//新建一個spring-service.xml文件
<!-- 掃描Service包下全部使用註解的類型 -->
<context:component-scan base-package="org.seckill.service"/>
// 注入Service依賴
    @Autowired //Spring一直使用的註解 自動在DAO中查找seckillDao 其它@Resource, @Inject
    private SeckillDao seckillDao;

    @Autowired
    private SuccessKilledDao successKilledDao;

配置並使用spring聲明式事務

目的:不用寫支持事務操做的代碼 交給第三方框架進行操做

流程:

  • 開啓事務
  • 修改SQL-1
  • 修改SQL-2
  • .....
  • 提交/回滾事務

ProxyFactoryBean + XML -> 早期2.0使用方式

tx: advice+aop命名空間方式 -> 一次配置永久生效

【推薦:掌握必定的控制權】註解@Transactional -> 註解控制

事務方法嵌套: 聲明式事務獨有的概念,和MySQL自己事務無關

傳播行爲默認爲propagation_required,當有一個新的事務加入進來,先判斷是不是已經存在的事務,若是已經存在直接加,不然新建一個新事物

何時回滾事務?

  • 當方法拋出運行期異常(RuntimeException) 能夠回滾 非運行期異常不會回滾
  • 當心不當的try-catch代碼

配置聲明式事務

bug log: 配置文件的順序不對,致使沒法定位<tx:annotation-driven transaction-manager="transactionManager"/> 語句

http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd

logback官方文檔配置
https://logback.qos.ch/manual/configuration.html

繼承測試的優化

try catch語句來改變動改集成測試的成功失敗斷定標準

//集成測試代碼
 @Test
    public void testSeckillLogic() {
        long id = 1000;
        Exposer exposer = seckillService.exportSeckillUrl(id);
        if (exposer.isExposed()) {
            logger.info("exposer={}",exposer);
            long phone = 12343232331L;
            // 將Md5的生成和使用集合到一塊兒
            String md5 = exposer.getMd5();
            // 定義兩個可容許的異常: 1.重複秒殺 和 2.秒殺已關閉異常 只提出警告而不認爲屬於測試失敗
            try {
                SeckillExecution execution = seckillService.executeSeckill(id, phone, md5);
                logger.info("result={}", execution);
            } catch (RepeatKillException e) {
                logger.error(e.getMessage());
            } catch (SeckillCloseException e) {
                logger.error(e.getMessage());
            }
        } else {
            // 秒殺未開啓
            logger.warn("exposer={}",exposer);
        }
    }


Web層開發

前端交互設計流程

交互設計
Restful - Swagger?
SpringMVC配置設計與實現
Bootstrap + jQuery

產品 -> 前端 -> 後端(存儲 + 展現)

前端頁面流程:
列表頁 -> 詳情頁 -> login -> 展現邏輯 ->
-> 登陸操做 ->寫入cookie ->展現邏輯

  • 詳情頁流程邏輯
    1.獲取標準系統時間(服務器端) 進行時間判斷
    開始以前:倒計時 不能夠獲得秒殺地址
    秒殺中:直接獲得秒殺地址 有一個秒殺按鈕 - 執行秒殺 - 後端交互
    結束以後:秒殺結束

Restful接口設計:
興起於Rails,一種優雅的URI表述方式,表示一種資源的狀態和狀態轉移

幾個示例:
GET /seckill/list
WRONG: POST /seckill/execute/{seckillId}
RIGHT: POST /seckill/{seckillId}/execution seckill:資源的模塊 Id:表示具體的秒殺資源 execution: 執行器 執行秒殺操做 用名詞 POST表示狀態的轉移

WRONG: GET /seckill/delete/{id}
RIGHT: DELETE /seckill/{id}/delete

Restful 規範:
GET: 查詢
POST: 添加和修改 非冪等
PUT: 修改操做 冪等
DELETE: 刪除操做

URL設計
/模塊/資源/{標識符}/集合{id}/
/user/{uid}/friends 好友列表
/user/{uid}/followers 粉絲列表

秒殺API設計
GET /seckill/list 秒殺列表
GET /seckill/{id}/detail 詳情頁
GET /seckill/time/now 系統時間
POST /seckill/{id}/exposer 暴露秒殺
POST /seckill/{id}/{md5}/execution 執行秒殺

基於系統業務進行設計 能夠工程師部門進行交互直接經過URL體現 接口使用者 外部系統 用戶


Spring MVC理論

圍繞Handler進行開發
Handler的產出包括:

  1. Model
  2. View

SpringMVC運行流程:
1.用戶發送請求 -> DispatcherServlet 攔截全部請求

  1. DispatcherServlet 使用DefaultAnnotationHandlerMapping進行URL的映射 URL -> Handler
  2. DefaultAnnotationHandlerAdapter 用來作Handler適配 -> 匹配咱們本身根據業務邏輯開發的代碼 SeckillController
  3. SeckillController 產出: ModelAndView 例如/seckill/list會產生一個秒殺產品的list頁面 並交給DispatcherServlet
  4. DispatcherServlet 經過InternalResourceViewResolver判斷這個list頁面和哪一個Model匹配,並將該Model和list.jsp文件進行結合
  5. 將list.jsp返回給用戶

HTTP請求地址映射原理

HTTP 請求先發送到 Servlet容器之中(如Tomcat, Jetty) SpringMVC有一個Mapping 用註解作映射 Annotation Handler Mapping -> 對應到一個Handler 方法之中

註解映射技巧

@RequestMapping註解:
1.支持標準的URL
2.Ant風格URL ?一個 *任意個 **任意URL路徑
3.帶{xxx}佔位符的URL
例如:
/user/*/creation 匹配 user/aaa/creation /user/bbb/creation 等
/user/{userId} 匹配 user/123 user/abc
/company/{companyId}/user/{userId}/detail 匹配 /company/123/user/456/detail

請求方法的細節處理

  1. 請求參數綁定
  2. 請求方式限制
  3. 請求轉發和重定向
  4. 數據模型賦值
  5. 如何返回JSON數據
  6. cookie的訪問
@RequestMapping(value="/{seckillId}/detail", method = RequestMethod.GET)
//@PathVariable 來完成URL參數的綁定 method = ... 來實現方法的綁定
public String detail(@PathVariable("seckillId") Long seckillId, Model model) {
    if (seckillId == null) {
//redirect 和 forward 關鍵詞表示衝頂縣仍是轉發
        return "redirect:/seckill/list";
    }
    Seckill seckill = seckillService.getById(seckillId);
    if (seckill == null) {
        return "forward:/seckill/list";
    }
    //model關鍵字調用
    model.addAttribute("seckill", seckill); //model
//默認配置將"detail"字符串轉換爲一個 jsp文件
    return "detail"; //view
//如何返回JSON類型文件
@RequestMapping(value = "/{seckillId}/{md5}/execution",
    method = RequestMethod.POST,
    produces = { "application/json;charset=UTF-8"}) //告訴瀏覽器這個application json類型
@ResponseBody
// Response Body就是返回JSON數據
public SeckillResult<Exposer> exportSeckillURL(@PathVariable("id") long id) {
    SeckillResult<Exposer> result; // 將數據封裝爲json 類型
    try {
        Exposer exposer = ...
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
        result = new SeckillResult<Exposer>(false, e.getMessage());
    }
    return result;
}
// Cookie訪問
@RequestMapping(value = "/{seckillId}/{md5}/execution", method = RequestMethod.POST)
public SeckillResult<SeckillExecution> execute(@PathVariable("seckillId") long seckillId,
                                                                  @PathVariable("md5") String md5,
                                                                  @CookieValue(value = "killPhone", required = false) Long phone)
{
    if (phone == null) {
        return new SeckillResult<SeckillExecution> (false, "未註冊電話");
    }
    SeckillExecution execution = seckillService.executeSeckillByProcedure(seckillId, phone, md5);
    SeckillResult<SeckillExecution> result = new SeckillResult<SeckillExecution>(true, execution);
    return result;
}

Tomcat 服務器部署配置
https://www.jianshu.com/p/07f1f99f73ad

先下載tomcat , 再將tomcat加入到intellij編譯器之中
war模式—-將WEB工程以包的形式上傳到服務器
war exploded模式—-將WEB工程以當前文件夾的位置關係上傳到服務器

靜態包含 與 動態包含:
靜態包含: 包含的jsp文件的內容會與主文件合併
動態包含: jsp會獨立servlet, 把servlet的運行結果與主文件運行結果合併

高併發優化

高併發發生在哪裏?

發生在詳情頁: 最感興趣的商品
詳情頁 系統時間 地址暴露接口 執行秒殺操做
爲何要單獨獲取系統時間?
詳情頁 -> 用戶大量刷新這個頁面 -> 詳情頁部署在CDN節點上 detail頁靜態化,靜態資源 css, js , 訪問detail頁面 和 css, js資源是不須要訪問秒殺系統,因此須要一個單獨拿到

Content Distribution Network 能夠是靜態資源 也能夠是動態資源 加速用戶獲取數據的系統
通常部署在離用戶最近的網絡節點上,
命中了CDN是不須要訪問到後端服務器
互聯網公司會本身搭建或租用

獲取系統時間不須要優化: Java訪問內存10ns, 1s = 10億納秒 = 每秒能夠作1億次

秒殺地址接口是在變化就不可以再CDN緩存,適合服務器緩存 redis( 10萬/qps, 集羣化以後能夠作百萬qps)
請求地址 -> redis -> Mysql 一致性維護: 超時穿透/ 主動更新

秒殺操做優化分析: 大部分的寫操做 和 全部核心操做沒有辦法進行緩存
後端緩存困難: 庫存問題 不可能在緩存之中進行減庫存操做
1行數據競爭, 熱點商品裏面的競爭, MySQL大量減庫存競爭
其它的方案分析:
執行秒殺 -> 原子計數器 用redis/NoSQL來實現 減庫存 就是在原子計數器上減
記錄行爲信息(誰減了庫存) -> 分佈式MQ 單個MQ能夠支持幾十萬qps
後端服務消費消息並落地 -> MySQL 當中

成本過高:
運維成本和穩定性: 支持分佈式組件
開發成本:
冪等性很難保證: 重複秒殺問題 當減庫存時,不知道該用戶是否秒殺過, 通常操做是維護另外一個MQ訪問方案來
不適合新手的架構: 微信搶紅包 淘寶秒殺
運維成本和穩定性: NoSQL 和 MQ

爲何不認爲MySQL解決? 認爲MySQL真的低效麼? update 壓力測試 1s 4000次
Java控制事務行爲分析 update table set num = num - 1 WHERE id = 10 and num > 0 insert一個購買明細
UPDATE table set num = num - 1 WHERE id = 10 and num > 0 等待行級鎖 得到鎖Lock
等待行級鎖
變爲一個串行化的操做

可能存在Java客戶端等待 Java和數據庫通訊之中存在網絡延遲 全部SQL+網絡+GC 多是2ms, 1s以內500次

每一個線程完成的操做:
update減庫存 網絡延遲問題 + GC
insert 購買明細: 網絡延遲 + GC
commit/rollback

優化分析: 行級鎖在commit以後釋放
延遲問題很關鍵

Java

優化總結:

  • 前端控制: 暴露接口,按鈕防重複
  • 動靜態數據分離: CDN緩存,
  • 事務競爭優化: 減小事務鎖時間 經過Java客戶端會有 start transaction update insert commit rollback都有網絡延遲優化

redis後端緩存優化編碼 詳情頁CDN 系統時間 不要優化, 倒計時: 瀏覽器端 地址暴露接口 執行秒殺操做

Seckill seckill = seckillDao.queryById(seckillId)

get from cache
if null
get db
else
put cache
logic code

序列化:
Java序列化對比
protostuff 建立序列化速度很高 字節數更少,

採用自定義序列化方式(開源社區方案)
portostuff-core
protostuff-runtime

public Seckill getSeckill(long seckillId) {
    try {
        Jedis jedis = jedisPool.getResource();
        try {
            String key = "seckill:" + seckillId;
            byte[] bytes = jedis.get(key.getBytes());
            if (bytes != null) {
                Seckill seckill = schema.newMessage();
                ProtostuffIOUtil.mergeFrom(bytes, seckill, schema);
                return seckill;
            }
        } finally {
            jedis.close();
        } 
    } catch
相關文章
相關標籤/搜索