主頁 > 後端開發 > 多級快取降低高并發壓力

多級快取降低高并發壓力

2023-02-16 07:01:36 後端開發

多級快取

簡介

1. 傳統快取

傳統的快取策略一般是請求到達Tomcat后,先查詢Redis,如果未命中則查詢資料庫,如圖:

image

存在下面的問題:

?由于redis的承受能力大于tomcat,所以請求要經過Tomcat處理,Tomcat的性能成為整個系統的瓶頸

?Redis快取失效時,會對資料庫產生沖擊

2.多級快取理論

多級快取就是充分利用請求處理的每個環節,分別添加快取,減輕Tomcat壓力,提升服務性能:

  • 瀏覽器訪問靜態資源時,優先讀取瀏覽器本地快取
  • 訪問非靜態資源(ajax查詢資料)時,訪問服務端
  • 請求到達Nginx后,優先讀取Nginx本地快取
  • 如果Nginx本地快取未命中,則去直接查詢Redis(不經過Tomcat)
  • 如果Redis查詢未命中,則查詢Tomcat
  • 請求進入Tomcat后,優先查詢JVM行程快取
  • 如果JVM行程快取未命中,則查詢資料庫

在多級快取架構中,Nginx內部需要撰寫本地快取查詢、Redis查詢、Tomcat查詢的業務邏輯,因此這樣的nginx服務不再是一個反向代理服務器,而是一個撰寫業務的Web服務器了

因此這樣的業務Nginx服務也需要搭建集群來提高并發,再有專門的nginx服務來做反向代理,我們的Tomcat服務將來也會部署為集群模式:
image

image

可見,多級快取的關鍵有兩個:

  • 一個是在nginx中撰寫業務,實作nginx本地快取、Redis、Tomcat的查詢

  • 另一個就是在Tomcat中實作JVM行程快取

其中Nginx編程則會用到OpenResty框架結合Lua這樣的語言,

JVM快取

Tomcat服務器的行程快取

1. Caffeine

1.1 介紹

快取在日常開發中啟動至關重要的作用,由于是存盤在記憶體中,資料的讀取速度是非常快的,能大量減少對資料庫的訪問,減少資料庫的壓力,我們把快取分為兩類:

  • 分布式快取,例如Redis:
    • 優點:存盤容量更大、可靠性更好、可以在集群間共享
    • 缺點:訪問快取有網路開銷
    • 場景:快取資料量較大、可靠性要求較高、需要在集群間共享
  • 行程本地快取,例如Caffeine、HashMap、GuavaCache:
    • 優點:讀取本地記憶體,沒有網路開銷,速度更快
    • 缺點:存盤容量有限、可靠性較低、無法共享
    • 場景:性能要求較高,快取資料量較小

Caffeine是一個基于Java8開發的,提供了近乎最佳命中率的高性能的本地快取庫,目前Spring內部的快取使用的就是Caffeine,GitHub地址:https://github.com/ben-manes/caffeine

Caffeine的性能非常好,下圖是官方給出的性能對比:可以看到Caffeine的性能遙遙領先!

image

Caffeine快取使用的基本API:put、get

@Test
void testBasicOps() {
    // 構建cache物件
    Cache<String, String> cache = Caffeine.newBuilder().build();

    // 存資料
    cache.put("detective", "柯南");

    // 取資料
    String detective = cache.getIfPresent("detective");
    System.out.println("detective = " + detective);

    // 取資料,包含兩個引數:
    // 引數一:快取的key
    // 引數二:Lambda運算式,運算式引數就是快取的key,方法體是查詢資料庫的邏輯
    // 優先根據key查詢JVM快取,如果未命中,則執行引數二的Lambda運算式
    String defaultGF = cache.get("defaultGF", key -> {
        // 根據key去資料庫查詢資料
        return "工藤新一";
    });
    System.out.println("defaultGF = " + defaultGF);
}
1.2 Caffeine快取清除策略

注意:在默認情況下,當一個快取元素過期的時候,Caffeine不會自動立即將其清理和驅逐,而是在一次讀或寫操作后,或者在空閑時間完成對失效資料的驅逐,

Caffeine提供了三種快取驅逐策略:

  • 基于容量:設定快取的數量上限

    // 創建快取物件
    Cache<String, String> cache = Caffeine.newBuilder()
        .maximumSize(1) // 設定快取大小上限為 1
        .build();	
    
  • 基于時間:設定快取的有效時間

    // 創建快取物件
    Cache<String, String> cache = Caffeine.newBuilder()
        // 設定快取有效期為 10 秒,從最后一次寫入開始計時 
        .expireAfterWrite(Duration.ofSeconds(10)) 
        .build();
    
    
  • 基于參考:設定快取為軟參考或弱參考,利用GC來回收快取資料,性能較差,不建議使用,

1.3 舉例:實作JVM快取

需求

利用Caffeine實作下列需求:

  • 給根據id查詢商品的業務添加快取,快取未命中時查詢資料庫
  • 給根據id查詢商品庫存的業務添加快取,快取未命中時查詢資料庫
  • 快取初始大小為100
  • 快取上限為10000

實作

首先,我們需要定義兩個Caffeine的快取物件,分別保存商品、庫存的快取資料,

在item-service的com.heima.item.config包下定義CaffeineConfig類:

  1. 定義一個配置類設定每個快取的資訊并創建
  2. 注入bean到spring容器中
package com.heima.item.config;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.heima.item.pojo.Item;
import com.heima.item.pojo.ItemStock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CaffeineConfig {

    @Bean
    public Cache<Long, Item> itemCache(){
        return Caffeine.newBuilder()
                .initialCapacity(100)
                .maximumSize(10_000)
                .build();
    }

    @Bean
    public Cache<Long, ItemStock> stockCache(){
        return Caffeine.newBuilder()
                .initialCapacity(100)
                .maximumSize(10_000)
                .build();
    }
}

然后,修改item-service中的com.heima.item.web包下的ItemController類,添加快取邏輯:

  1. 使用前面注入的bean物件
  2. 從快取中get得到快取資料,若快取中沒有就從資料庫中得到資料并快取到JVM中
  3. 回傳get得到的快取資料
@RestController
@RequestMapping("item")
public class ItemController {

    @Autowired
    private IItemService itemService;
    @Autowired
    private IItemStockService stockService;

    @Autowired
    private Cache<Long, Item> itemCache;
    @Autowired
    private Cache<Long, ItemStock> stockCache;
    
    // ...其它略
    
    @GetMapping("/{id}")
    public Item findById(@PathVariable("id") Long id) {
        //該方法是獲取,如果jvm快取中沒有就從資料庫中得到資料并快取到JVM中
        return itemCache.get(id, key -> itemService.query() 
                .ne("status", 3).eq("id", key)
                .one()
        );
    }

    @GetMapping("/stock/{id}")
    public ItemStock findStockById(@PathVariable("id") Long id) {
        return stockCache.get(id, key -> stockService.getById(key));
    }
}

Lua

CentOS7默認已經安裝了Lua語言環境,所以可以直接運行Lua代碼,

Nginx編程需要用到Lua語言,因此我們必須先入門Lua的基本語法,

1. 介紹

Lua 是一種輕量小巧的腳本語言,用標準C語言撰寫并以源代碼形式開放, 其設計目的是為了嵌入應用程式中,從而為應用程式提供靈活的擴展和定制功能,官網:https://www.lua.org/

image

Lua經常嵌入到C語言開發的程式中,例如游戲開發、游戲插件等,

Nginx本身也是C語言開發,因此也允許基于Lua做拓展,

2. 語法

2.1 基本使用

CentOS7默認已經安裝了Lua語言環境,所以可以直接運行Lua代碼,

1)在Linux虛擬機的任意目錄下,新建一個hello.lua檔案

image

2)添加下面的內容

print("Hello World!")  

3)運行

image

2.2 Lua資料型別

Lua中支持的常見資料型別包括:

image

另外,Lua提供了type()函式來判斷一個變數的資料型別:

image

2.3 宣告變數

Lua宣告變數的時候無需指定資料型別,而是用local來宣告變數為區域變數:

-- 宣告字串,可以用單引號或雙引號,
local str = 'hello'
-- 字串拼接可以使用 ..
local str2 = 'hello' .. 'world'
-- 宣告數字
local num = 21
-- 宣告布爾型別
local flag = true

Lua中的table型別既可以作為陣列,又可以作為Java中的map來使用,陣列就是特殊的table,key是陣列角標而已:

-- 宣告陣列 ,key為角標的 table
local arr = {'java', 'python', 'lua'}
-- 宣告table,類似java的map
local map =  {name='Jack', age=21}

Lua中的陣列角標是從1開始,訪問的時候與Java中類似:

-- 訪問陣列,lua陣列的角標從1開始
print(arr[1])

Lua中的table可以用key來訪問:

-- 訪問table
print(map['name'])
print(map.name)

2.4 回圈

類似于java的MAP,只是代表陣列的時候鍵是陣列下標,值是陣列值

對于table,我們可以利用for回圈來遍歷,不過陣列和普通table遍歷略有差異,

遍歷陣列:

-- 宣告陣列 key為索引的 table
local arr = {'java', 'python', 'lua'}
-- 遍歷陣列:index是陣列下標,value是當前回圈的值,在方法體里可呼叫
for index,value in ipairs(arr) do
    print(index, value) 
end

遍歷普通table

-- 宣告map,也就是table
local map = {name='Jack', age=21}
-- 遍歷table:key是map的key,value是map的value,在方法體里可呼叫
for key,value in pairs(map) do
   print(key, value) 
end

2.5 函式

定義函式的語法:

function 函式名( argument1, argument2..., argumentn)
    -- 函式體
    return 回傳值
end

例如,定義一個函式,用來列印陣列:

function printArr(arr)
    for index, value in ipairs(arr) do
        print(value)
    end
end

2.6 條件控制

類似Java的條件控制,例如if、else語法:

if(布爾運算式)
then
   --[ 布爾運算式為 true 時執行該陳述句塊 --]
else
   --[ 布爾運算式為 false 時執行該陳述句塊 --]
end

與java不同,布爾運算式中的邏輯運算是基于英文單詞:

image

實作多級快取

1. OpenResty

1.1 簡介

OpenResty是Nginx的高性能web開發平臺,使用的是Lua語言來實作業務

多級快取的實作離不開Nginx編程,而Nginx編程又離不開OpenResty,

OpenResty? 是一個基于 Nginx的高性能 Web 平臺,用于方便地搭建能夠處理超高并發、擴展性極高的動態 Web 應用、Web 服務和動態網關,具備下列特點:

  • 具備Nginx的完整功能
  • 基于Lua語言進行擴展,集成了大量精良的 Lua 庫、第三方模塊
  • 允許使用Lua自定義業務邏輯自定義庫

官方網站: https://openresty.org/cn/

image

1.2 安裝部署OpenResty

1.2.1 安裝

首先你的Linux虛擬機必須聯網

1)安裝開發庫

首先要安裝OpenResty的依賴開發庫,執行命令:

yum install -y pcre-devel openssl-devel gcc --skip-broken
2)安裝OpenResty倉庫

你可以在你的 CentOS 系統中添加 openresty 倉庫,這樣就可以便于未來安裝或更新我們的軟體包(通過 yum check-update 命令),運行下面的命令就可以添加我們的倉庫:

yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo

如果提示說命令不存在,則運行:

yum install -y yum-utils 

然后再重復上面的命令

3)安裝OpenResty

然后就可以像下面這樣安裝軟體包,比如 openresty

yum install -y openresty
4)安裝opm工具

opm是OpenResty的一個管理工具,可以幫助我們安裝一個第三方的Lua模塊,

如果你想安裝命令列工具 opm,那么可以像下面這樣安裝 openresty-opm 包:

yum install -y openresty-opm
5)目錄結構

默認情況下,OpenResty安裝的目錄是:/usr/local/openresty

image

看到里面的nginx目錄了嗎,OpenResty就是在Nginx基礎上集成了一些Lua模塊,

6)配置nginx的環境變數

打開組態檔:

vi /etc/profile

在最下面加入兩行:

export NGINX_HOME=/usr/local/openresty/nginx
export PATH=${NGINX_HOME}/sbin:$PATH

NGINX_HOME:后面是OpenResty安裝目錄下的nginx的目錄

然后讓配置生效:

source /etc/profile
1.2.2 啟動和運行

OpenResty底層是基于Nginx的,查看OpenResty目錄的nginx目錄,結構與windows中安裝的nginx基本一致:

image

所以運行方式與nginx基本一致:

# 啟動nginx
nginx
# 重新加載配置
nginx -s reload
# 停止
nginx -s stop

nginx的默認組態檔注釋太多,影響后續我們的編輯,這里將nginx.conf中的注釋部分洗掉,保留有效部分,

修改/usr/local/openresty/nginx/conf/nginx.conf檔案,內容如下:

#user  nobody;
worker_processes  1;
error_log  logs/error.log;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;

    server {
        listen       8081;
        server_name  localhost;
        location / {
            root   html;
            index  index.html index.htm;
        }
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}

在Linux的控制臺輸入命令以啟動nginx:

nginx

然后訪問頁面:http://192.168.194.132:8081,注意ip地址替換為你自己的虛擬機IP:

1.2.4 備注

加載OpenResty的lua模塊:

#lua 模塊
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
#c模塊     
lua_package_cpath "/usr/local/openresty/lualib/?.so;;";  

common.lua

-- 封裝函式,發送http請求,并決議回應
local function read_http(path, params)
    local resp = ngx.location.capture(path,{
        method = ngx.HTTP_GET,
        args = params,
    })
    if not resp then
        -- 記錄錯誤資訊,回傳404
        ngx.log(ngx.ERR, "http not found, path: ", path , ", args: ", args)
        ngx.exit(404)
    end
    return resp.body
end
-- 將方法匯出
local _M = {  
    read_http = read_http
}  
return _M

釋放Redis連接API:

-- 關閉redis連接的工具方法,其實是放入連接池
local function close_redis(red)
    local pool_max_idle_time = 10000 -- 連接的空閑時間,單位是毫秒
    local pool_size = 100 --連接池大小
    local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)
    if not ok then
        ngx.log(ngx.ERR, "放入redis連接池失敗: ", err)
    end
end

讀取Redis資料的API:

-- 查詢redis的方法 ip和port是redis地址,key是查詢的key
local function read_redis(ip, port, key)
    -- 獲取一個連接
    local ok, err = red:connect(ip, port)
    if not ok then
        ngx.log(ngx.ERR, "連接redis失敗 : ", err)
        return nil
    end
    -- 查詢redis
    local resp, err = red:get(key)
    -- 查詢失敗處理
    if not resp then
        ngx.log(ngx.ERR, "查詢Redis失敗: ", err, ", key = " , key)
    end
    --得到的資料為空處理
    if resp == ngx.null then
        resp = nil
        ngx.log(ngx.ERR, "查詢Redis資料為空, key = ", key)
    end
    close_redis(red)
    return resp
end

開啟共享詞典:

# 共享字典,也就是本地快取,名稱叫做:item_cache,大小150m
lua_shared_dict item_cache 150m; 

1.3 OpenResty快速入門

我們希望達到的多級快取架構如圖:

image

其中:

  • windows上的nginx用來做反向代理服務,將前端的查詢商品的ajax請求代理到OpenResty集群

  • OpenResty集群用來撰寫多級快取業務

1.3.1 反向代理配置
1)OpenResty監聽請求

OpenResty的很多功能都依賴于其目錄下的Lua庫,需要在nginx.conf中指定依賴庫的目錄,并匯入依賴:

1)添加對OpenResty的Lua模塊的加載

修改/usr/local/openresty/nginx/conf/nginx.conf檔案,在其中的http下面,添加下面代碼:

#lua 模塊
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
#c模塊     
lua_package_cpath "/usr/local/openresty/lualib/?.so;;";  

2)監聽/api/item路徑

修改/usr/local/openresty/nginx/conf/nginx.conf檔案,在nginx.conf的server下面,添加對/api/item這個路徑的監聽:

location  /api/item {
    # 默認的回應型別
    default_type application/json;
    # 回應結果由lua/item.lua檔案來決定
    content_by_lua_file lua/item.lua;
}

這個監聽,就類似于SpringMVC中的@GetMapping("/api/item")做路徑映射,

content_by_lua_file lua/item.lua則相當于呼叫item.lua這個檔案,執行其中的業務,把結果回傳給用戶,相當于java中呼叫service,

2)撰寫item.lua

1)在/usr/loca/openresty/nginx目錄創建檔案夾:lua

image

2)在/usr/loca/openresty/nginx/lua檔案夾下,新建檔案:item.lua

image

3)撰寫item.lua,回傳假資料

item.lua中,利用ngx.say()函式回傳資料到Response中

ngx.say('{"id":10001,"name":"SALSA AIR","title":"RIMOWA 21寸托運箱拉桿箱 SALSA AIR系列果綠色 820.70.36.4","price":17900,"image":"https://m.360buyimg.com/mobilecms/s720x720_jfs/t6934/364/1195375010/84676/e9f2c55f/597ece38N0ddcbc77.jpg!q70.jpg.webp","category":"拉桿箱","brand":"RIMOWA","spec":"","status":1,"createTime":"2019-04-30T16:00:00.000+00:00","updateTime":"2019-04-30T16:00:00.000+00:00","stock":2999,"sold":31290}')

4)重新加載配置

nginx -s reload
1.3.2 請求引數處理
① 獲取引數的API

OpenResty中提供了一些API用來獲取不同型別的前端請求引數:

image

② 獲取引數并回傳

在前端發起的ajax請求如圖:

image

可以看到商品id是以路徑占位符方式傳遞的,因此可以利用正則運算式匹配的方式來獲取ID

1)獲取商品id

修改/usr/loca/openresty/nginx/nginx.conf檔案中監聽/api/item的代碼,利用正則運算式獲取ID:

location ~ /api/item/(\d+) {
    # 默認的回應型別
    default_type application/json;
    # 回應結果由lua/item.lua檔案來決定
    content_by_lua_file lua/item.lua;
}

2)拼接ID并回傳

修改/usr/loca/openresty/nginx/lua/item.lua檔案,獲取id并拼接到結果中回傳:

-- 獲取商品id
local id = ngx.var[1]
-- 拼接并回傳
ngx.say('{"id":' .. id .. ',"name":"SALSA AIR","title":"RIMOWA 21寸托運箱拉桿箱 SALSA AIR系列果綠色 820.70.36.4","price":17900,"image":"https://m.360buyimg.com/mobilecms/s720x720_jfs/t6934/364/1195375010/84676/e9f2c55f/597ece38N0ddcbc77.jpg!q70.jpg.webp","category":"拉桿箱","brand":"RIMOWA","spec":"","status":1,"createTime":"2019-04-30T16:00:00.000+00:00","updateTime":"2019-04-30T16:00:00.000+00:00","stock":2999,"sold":31290}')

3)重新加載并測驗

運行命令以重新加載OpenResty配置:

nginx -s reload

2. Nginx直接查詢Tomcat

拿到商品ID后,本應去快取中查詢商品資訊,不過目前我們還未建立nginx、redis快取,因此,這里我們先根據商品id去tomcat查詢商品資訊,我們實作如圖部分:

image

需要注意的是,我們的OpenResty是在虛擬機,Tomcat是在Windows電腦上,兩者IP一定不要搞錯了,

image

2.1 發送http請求的API

nginx提供了內部API用以發送http請求:

local resp = ngx.location.capture("/path",{
    method = ngx.HTTP_GET,   -- 請求方式
    args = {a=1,b=2},  -- get方式傳引數
})

回傳的回應內容包括:

  • resp.status:回應狀態碼
  • resp.header:回應頭,是一個table
  • resp.body:回應體,就是回應資料

注意:這里的path是路徑,并不包含IP和埠,這個請求會被nginx內部的server監聽并處理,

但是我們希望這個請求發送到Tomcat服務器,所以還需要撰寫一個server來對這個路徑做反向代理:

 location /path {
     # 這里是windows電腦的ip和Java服務埠,需要確保windows防火墻處于關閉狀態
     proxy_pass http://192.168.150.1:8081; 
 }

原理如圖:

image

2.2 封裝http工具

下面,我們封裝一個發送Http請求的工具,基于ngx.location.capture來實作查詢tomcat,

1)添加反向代理,到windows的Java服務

因為item-service中的介面都是/item開頭,所以我們監聽/item路徑,代理到windows上的tomcat服務,

修改 /usr/local/openresty/nginx/conf/nginx.conf檔案,添加一個location:

location /item {
    proxy_pass http://192.168.150.1:8081;
}

以后,只要我們呼叫ngx.location.capture("/item"),就一定能發送請求到windows的tomcat服務,

2)封裝工具類

之前我們說過,OpenResty啟動時會加載以下兩個目錄中的工具檔案:

image

所以,自定義的http工具也需要放到這個目錄下,

/usr/local/openresty/lualib目錄下,新建一個common.lua檔案:

vi /usr/local/openresty/lualib/common.lua

內容如下:

-- 封裝函式,發送http請求,并決議回應
local function read_http(path, params)
    local resp = ngx.location.capture(path,{
        method = ngx.HTTP_GET,
        args = params,
    })
    if not resp then
        -- 記錄錯誤資訊,回傳404
        ngx.log(ngx.ERR, "http請求查詢失敗, path: ", path , ", args: ", args)
        ngx.exit(404)
    end
    return resp.body
end
-- 將方法匯出
local _M = {  
    read_http = read_http
}  
return _M

這個工具將read_http函式封裝到_M這個table型別的變數中,并且回傳,這類似于匯出,

使用的時候,可以利用require('common')來匯入該函式庫,這里的common是函式庫的檔案名,

3)實作商品查詢

最后,我們修改/usr/local/openresty/lua/item.lua檔案,利用剛剛封裝的函式庫實作對tomcat的查詢:

-- 引入自定義common工具模塊,回傳值是common中回傳的 _M
local common = require("common")
-- 從 common中獲取read_http這個函式
local read_http = common.read_http
-- 獲取路徑引數
local id = ngx.var[1]
-- 根據id查詢商品
local itemJSON = read_http("/item/".. id, nil)
-- 根據id查詢商品庫存
local itemStockJSON = read_http("/item/stock/".. id, nil)

這里查詢到的結果是json字串,并且包含商品、庫存兩個json字串,頁面最終需要的是把兩個json拼接為一個json:

image

這就需要我們先把JSON變為lua的table,完成資料整合后,再轉為JSON,

2.3 CJSON工具類

OpenResty提供了一個cjson的模塊用來處理JSON的序列化和反序列化,

官方地址: https://github.com/openresty/lua-cjson/

1)引入cjson模塊:

local cjson = require "cjson"

2)序列化:

local obj = {
    name = 'jack',
    age = 21
}
-- 把 table 序列化為 json
local json = cjson.encode(obj)

3)反序列化:

local json = '{"name": "jack", "age": 21}'
-- 反序列化 json為 table
local obj = cjson.decode(json);
print(obj.name)

2.4 實作Tomcat查詢

下面,我們修改之前的item.lua中的業務,添加json處理功能:

-- 匯入common函式庫
local common = require('common')
local read_http = common.read_http
-- 匯入cjson庫
local cjson = require('cjson')

-- 獲取路徑引數
local id = ngx.var[1]
-- 根據id查詢商品
local itemJSON = read_http("/item/".. id, nil)
-- 根據id查詢商品庫存
local itemStockJSON = read_http("/item/stock/".. id, nil)

-- JSON轉化為lua的table
local item = cjson.decode(itemJSON)
local stock = cjson.decode(itemStockJSON)

-- 組合資料
item.stock = stock.stock
item.sold = stock.sold

-- 把item序列化為json 回傳結果
ngx.say(cjson.encode(item))

2.5 基于ID負載均衡

剛才的代碼中,我們的tomcat是單機部署,而實際開發中,tomcat一定是集群模式:

image

因此,OpenResty需要對tomcat集群做負載均衡,

而默認的負載均衡規則是輪詢模式,當我們查詢/item/10001時:

  • 第一次會訪問8081埠的tomcat服務,在該服務內部就形成了JVM行程快取
  • 第二次會訪問8082埠的tomcat服務,該服務內部沒有JVM快取(因為JVM快取無法共享),會查詢資料庫
  • ...

你看,因為輪詢的原因,第一次查詢8081形成的JVM快取并未生效,直到下一次再次訪問到8081時才可以生效,快取命中率太低了,

怎么辦?

如果能讓同一個商品,每次查詢時都訪問同一個tomcat服務,那么JVM快取就一定能生效了,

也就是說,我們需要根據商品id做負載均衡,而不是輪詢,

1)原理

nginx提供了基于請求路徑做負載均衡的演算法:

nginx根據請求路徑做hash運算,把得到的數值對tomcat服務的數量取余,余數是幾,就訪問第幾個服務,實作負載均衡,

例如:

  • 我們的請求路徑是 /item/10001
  • tomcat總數為2臺(8081、8082)
  • 對請求路徑/item/1001做hash運算求余的結果為1
  • 則訪問第一個tomcat服務,也就是8081

只要id不變,每次hash運算結果也不會變,那就可以保證同一個商品,一直訪問同一個tomcat服務,確保JVM快取生效,

2)實作

修改/usr/local/openresty/nginx/conf/nginx.conf檔案,實作基于ID做負載均衡,

首先,定義tomcat集群,并設定基于路徑做負載均衡:

upstream tomcat-cluster {
    hash $request_uri;
    server 192.168.150.1:8081;
    server 192.168.150.1:8082;
}

然后,修改對tomcat服務的反向代理,目標指向tomcat集群:

location /item {
    proxy_pass http://tomcat-cluster;
}

重新加載OpenResty

nginx -s reload
3)測驗

啟動兩臺tomcat服務:

image

同時啟動:

image

清空日志后,再次訪問頁面,可以看到不同id的商品,訪問到了不同的tomcat服務:

image

image

3. Redis快取預熱

Redis快取會面臨冷啟動問題:

冷啟動:服務剛剛啟動時,Redis中并沒有快取,如果所有商品資料都在第一次查詢時添加快取,可能會給資料庫帶來較大壓力,

快取預熱:在實際開發中,我們可以利用大資料統計用戶訪問的熱點資料,在專案啟動時將這些熱點資料提前查詢并保存到Redis中,

我們資料量較少,并且沒有資料統計相關功能,目前可以在啟動時將所有資料都放入快取中,

1)利用Docker安裝Redis

docker run --name redis -p 6379:6379 -d redis redis-server --appendonly yes

2)在item-service服務中引入Redis依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

3)配置Redis地址

spring:
  redis:
    host: 192.168.150.101

4)撰寫初始化類

快取預熱需要在專案啟動時完成,并且必須是拿到RedisTemplate之后,

這里我們利用InitializingBean介面來實作,因為InitializingBean可以在類的bean創建完并且@Autowired成員變數全部注入后執行,

package com.heima.item.config;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.heima.item.pojo.Item;
import com.heima.item.pojo.ItemStock;
import com.heima.item.service.IItemService;
import com.heima.item.service.IItemStockService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class RedisHandler implements InitializingBean {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private IItemService itemService;
    @Autowired
    private IItemStockService stockService;

    private static final ObjectMapper MAPPER = new ObjectMapper();

    @Override
    public void afterPropertiesSet() throws Exception {
        // 初始化快取
        // 1.查詢商品資訊
        List<Item> itemList = itemService.list();
        // 2.放入快取
        for (Item item : itemList) {
            // 2.1.item序列化為JSON
            String json = MAPPER.writeValueAsString(item);
            // 2.2.存入redis
            redisTemplate.opsForValue().set("item:id:" + item.getId(), json);
        }

        // 3.查詢商品庫存資訊
        List<ItemStock> stockList = stockService.list();
        // 4.放入快取
        for (ItemStock stock : stockList) {
            // 2.1.item序列化為JSON
            String json = MAPPER.writeValueAsString(stock);
            // 2.2.存入redis
            redisTemplate.opsForValue().set("item:stock:id:" + stock.getId(), json);
        }
    }
}

4. 查詢Redis快取

現在,Redis快取已經準備就緒,我們可以再OpenResty中實作查詢Redis的邏輯了,如下圖紅框所示:

image

當請求進入OpenResty之后:

  • 優先查詢Redis快取
  • 如果Redis快取未命中,再查詢Tomcat

4.1 封裝Redis工具

OpenResty提供了操作Redis的模塊,我們只要引入該模塊就能直接使用,但是為了方便,我們將Redis操作封裝到之前的common.lua工具庫中,

修改/usr/local/openresty/lualib/common.lua檔案:

1)引入Redis模塊,并初始化Redis物件

-- 匯入redis
local redis = require('resty.redis')
-- 初始化redis
local red = redis:new()
red:set_timeouts(1000, 1000, 1000)

2)封裝函式,用來釋放Redis連接,其實是放入連接池

-- 關閉redis連接的工具方法,其實是放入連接池
local function close_redis(red)
    local pool_max_idle_time = 10000 -- 連接的空閑時間,單位是毫秒
    local pool_size = 100 --連接池大小
    local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)
    if not ok then
        ngx.log(ngx.ERR, "放入redis連接池失敗: ", err)
    end
end

3)封裝函式,根據key查詢Redis資料

-- 查詢redis的方法 ip和port是redis地址,key是查詢的key
local function read_redis(ip, port, key)
    -- 獲取一個連接
    local ok, err = red:connect(ip, port)
    if not ok then
        ngx.log(ngx.ERR, "連接redis失敗 : ", err)
        return nil
    end
    -- 查詢redis
    local resp, err = red:get(key)
    -- 查詢失敗處理
    if not resp then
        ngx.log(ngx.ERR, "查詢Redis失敗: ", err, ", key = " , key)
    end
    --得到的資料為空處理
    if resp == ngx.null then
        resp = nil
        ngx.log(ngx.ERR, "查詢Redis資料為空, key = ", key)
    end
    close_redis(red)
    return resp
end

4)匯出

-- 將方法匯出
local _M = {  
    read_http = read_http,
    read_redis = read_redis
}  
return _M

完整的common.lua:

-- 匯入redis
local redis = require('resty.redis')
-- 初始化redis
local red = redis:new()
red:set_timeouts(1000, 1000, 1000)

-- 關閉redis連接的工具方法,其實是放入連接池
local function close_redis(red)
    local pool_max_idle_time = 10000 -- 連接的空閑時間,單位是毫秒
    local pool_size = 100 --連接池大小
    local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)
    if not ok then
        ngx.log(ngx.ERR, "放入redis連接池失敗: ", err)
    end
end

-- 查詢redis的方法 ip和port是redis地址,key是查詢的key
local function read_redis(ip, port, key)
    -- 獲取一個連接
    local ok, err = red:connect(ip, port)
    if not ok then
        ngx.log(ngx.ERR, "連接redis失敗 : ", err)
        return nil
    end
    -- 查詢redis
    local resp, err = red:get(key)
    -- 查詢失敗處理
    if not resp then
        ngx.log(ngx.ERR, "查詢Redis失敗: ", err, ", key = " , key)
    end
    --得到的資料為空處理
    if resp == ngx.null then
        resp = nil
        ngx.log(ngx.ERR, "查詢Redis資料為空, key = ", key)
    end
    close_redis(red)
    return resp
end

-- 封裝函式,發送http請求,并決議回應
local function read_http(path, params)
    local resp = ngx.location.capture(path,{
        method = ngx.HTTP_GET,
        args = params,
    })
    if not resp then
        -- 記錄錯誤資訊,回傳404
        ngx.log(ngx.ERR, "http查詢失敗, path: ", path , ", args: ", args)
        ngx.exit(404)
    end
    return resp.body
end
-- 將方法匯出
local _M = {  
    read_http = read_http,
    read_redis = read_redis
}  
return _M

4.2 實作Redis查詢

接下來,我們就可以去修改item.lua檔案,實作對Redis的查詢了,

查詢邏輯是:

  • 根據id查詢Redis
  • 如果查詢失敗則繼續查詢Tomcat
  • 將查詢結果回傳

1)修改/usr/local/openresty/lua/item.lua檔案,添加一個查詢函式:

-- 匯入common函式庫
local common = require('common')
local read_http = common.read_http
local read_redis = common.read_redis
-- 封裝查詢函式
function read_data(key, path, params)
    -- 查詢本地快取
    local val = read_redis("127.0.0.1", 6379, key)
    -- 判斷查詢結果
    if not val then
        ngx.log(ngx.ERR, "redis查詢失敗,嘗試查詢http, key: ", key)
        -- redis查詢失敗,去查詢http
        val = read_http(path, params)
    end
    -- 回傳資料
    return val
end

2)而后修改商品查詢、庫存查詢的業務:

image

3)完整的item.lua代碼:

-- 匯入common函式庫
local common = require('common')
local read_http = common.read_http
local read_redis = common.read_redis
-- 匯入cjson庫
local cjson = require('cjson')

-- 封裝查詢函式
function read_data(key, path, params)
    -- 查詢本地快取
    local val = read_redis("127.0.0.1", 6379, key)
    -- 判斷查詢結果
    if not val then
        ngx.log(ngx.ERR, "redis查詢失敗,嘗試查詢http, key: ", key)
        -- redis查詢失敗,去查詢http
        val = read_http(path, params)
    end
    -- 回傳資料
    return val
end

-- 獲取路徑引數
local id = ngx.var[1]

-- 查詢商品資訊
local itemJSON = read_data("item:id:" .. id,  "/item/" .. id, nil)
-- 查詢庫存資訊
local stockJSON = read_data("item:stock:id:" .. id, "/item/stock/" .. id, nil)

-- JSON轉化為lua的table
local item = cjson.decode(itemJSON)
local stock = cjson.decode(stockJSON)
-- 組合資料
item.stock = stock.stock
item.sold = stock.sold

-- 把item序列化為json 回傳結果
ngx.say(cjson.encode(item))

5. Nginx本地快取

由于Nginx快取是時間清除策略,所以Nginx一般是存基本不變的資訊(店鋪資訊),不存經常改變的訊息(庫存數量)

現在,整個多級快取中只差最后一環,也就是nginx的本地快取了,如圖:

image

5.1 本地快取API

OpenResty為Nginx提供了shard dict的功能,可以在nginx的多個worker之間共享資料,實作快取功能,

1)開啟共享字典,在nginx.conf的http下添加配置:

 # 共享字典,也就是本地快取,名稱叫做:item_cache,大小150m
 lua_shared_dict item_cache 150m; 

2)操作共享字典:

-- 獲取本地快取物件
local item_cache = ngx.shared.item_cache
-- 存盤, 指定key、value、過期時間,單位s,默認為0代表永不過期
item_cache:set('key', 'value', 1000)
-- 讀取
local val = item_cache:get('key')

5.2 實作本地快取查詢

1)修改/usr/local/openresty/lua/item.lua檔案,修改read_data查詢函式,添加本地快取邏輯:

-- 匯入共享詞典,本地快取
local item_cache = ngx.shared.item_cache

-- 封裝查詢函式
function read_data(key, expire, path, params)
    -- 查詢本地快取
    local val = item_cache:get(key)
    if not val then
        ngx.log(ngx.ERR, "本地快取查詢失敗,嘗試查詢Redis, key: ", key)
        -- 查詢redis
        val = read_redis("127.0.0.1", 6379, key)
        -- 判斷查詢結果
        if not val then
            ngx.log(ngx.ERR, "redis查詢失敗,嘗試查詢http, key: ", key)
            -- redis查詢失敗,去查詢http
            val = read_http(path, params)
        end
    end
    -- 查詢成功,把資料寫入本地快取
    item_cache:set(key, val, expire)
    -- 回傳資料
    return val
end

2)修改item.lua中查詢商品和庫存的業務,實作最新的read_data函式:

image

其實就是多了快取時間引數,過期后nginx快取會自動洗掉,下次訪問即可更新快取,

這里給商品基本資訊設定超時時間為30分鐘,庫存為1分鐘,

因為庫存更新頻率較高,如果快取時間過長,可能與資料庫差異較大,

3)完整的item.lua檔案:

-- 匯入common函式庫
local common = require('common')
local read_http = common.read_http
local read_redis = common.read_redis
-- 匯入cjson庫
local cjson = require('cjson')
-- 匯入共享詞典,本地快取
local item_cache = ngx.shared.item_cache

-- 封裝查詢函式
function read_data(key, expire, path, params)
    -- 查詢本地快取
    local val = item_cache:get(key)
    if not val then
        ngx.log(ngx.ERR, "本地快取查詢失敗,嘗試查詢Redis, key: ", key)
        -- 查詢redis
        val = read_redis("127.0.0.1", 6379, key)
        -- 判斷查詢結果
        if not val then
            ngx.log(ngx.ERR, "redis查詢失敗,嘗試查詢http, key: ", key)
            -- redis查詢失敗,去查詢http
            val = read_http(path, params)
        end
    end
    -- 查詢成功,把資料寫入本地快取
    item_cache:set(key, val, expire)
    -- 回傳資料
    return val
end

-- 獲取路徑引數
local id = ngx.var[1]

-- 查詢商品資訊
local itemJSON = read_data("item:id:" .. id, 1800,  "/item/" .. id, nil)
-- 查詢庫存資訊
local stockJSON = read_data("item:stock:id:" .. id, 60, "/item/stock/" .. id, nil)

-- JSON轉化為lua的table
local item = cjson.decode(itemJSON)
local stock = cjson.decode(stockJSON)
-- 組合資料
item.stock = stock.stock
item.sold = stock.sold

-- 把item序列化為json 回傳結果
ngx.say(cjson.encode(item))

快取同步

快取資料同步的常見方式有三種:

設定有效期:給快取設定有效期,到期后自動洗掉,再次查詢時更新【nginx快取】

  • 優勢:簡單、方便
  • 缺點:時效性差,快取過期之前可能不一致
  • 場景:更新頻率較低,時效性要求低的業務

同步雙寫:在修改資料庫的同時,直接修改快取【每次增刪改都同時修改資料和快取】

  • 優勢:時效性強,快取與資料庫強一致
  • 缺點:有代碼侵入,耦合度高;
  • 場景:對一致性、時效性要求較高的快取資料

異步通知:修改資料庫時發送事件通知,相關服務監聽到通知后修改快取資料【以下兩種】

  • 優勢:低耦合,可以同時通知多個快取服務
  • 缺點:時效性一般,可能存在中間不一致狀態
  • 場景:時效性要求一般,有多個服務需要同步

而異步實作又可以基于MQ或者Canal來實作:

1)基于MQ的異步通知:

image

解讀:

  • 商品服務完成對資料的修改后,只需要發送一條訊息到MQ中,
  • 快取服務監聽MQ訊息,然后完成對快取的更新

依然有少量的代碼侵入,

2)基于Canal的通知

image

解讀:

  • 商品服務完成商品修改后,業務直接結束,沒有任何代碼侵入
  • Canal監聽MySQL變化,當發現變化后,立即通知快取服務
  • 快取服務接收到canal通知,更新快取

代碼零侵入

1. canal簡介

Canal [k?'n?l],譯意為水道/管道/溝渠,canal是阿里巴巴旗下的一款開源專案,基于Java開發,基于資料庫增量日志決議,提供增量資料訂閱&消費,GitHub的地址:https://github.com/alibaba/canal

Canal是基于mysql的主從同步來實作的,MySQL主從同步的原理如下:

image

  • 1)MySQL master 將資料變更寫入二進制日志( binary log),其中記錄的資料叫做binary log events
  • 2)MySQL slave 將 master 的 binary log events拷貝到它的中繼日志(relay log)
  • 3)MySQL slave 重放 relay log 中事件,將資料變更反映它自己的資料

而Canal就是把自己偽裝成MySQL的一個slave節點,從而監聽master的binary log變化,再把得到的變化資訊通知給Canal的客戶端,進而完成對其它資料庫的同步,

image

2. 安裝canal

2.1 開啟MySQL主從

Canal是基于MySQL的主從同步功能,因此必須先開啟MySQL的主從功能才可以(一定要先開啟后才能繼續)

2.1.1 開啟binlog

打開mysql容器掛載的日志檔案,我的在/tmp/mysql/conf目錄:

image

修改檔案:

vi /tmp/mysql/conf/my.cnf

添加內容:

log-bin=/var/lib/mysql/mysql-bin
binlog-do-db=heima

配置解讀:

  • log-bin=/var/lib/mysql/mysql-bin:設定binary log檔案的存放地址和檔案名,叫做mysql-bin
  • binlog-do-db=heima:指定對哪個database記錄binary log events,這里記錄heima這個庫

最終效果:

[mysqld]
skip-name-resolve
character_set_server=utf8
datadir=/var/lib/mysql
server-id=1000
log-bin=/var/lib/mysql/mysql-bin
binlog-do-db=heima
2.1.2 設定用戶權限

接下來添加一個僅用于資料同步的賬戶,出于安全考慮,這里僅提供對heima這個庫的操作權限,

create user canal@'%' IDENTIFIED by 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%' identified by 'canal';
FLUSH PRIVILEGES;

重啟mysql容器即可

docker restart mysql

測驗設定是否成功:在mysql控制臺,或者Navicat中,輸入命令:

show master status;

image

2.2 安裝Canal

2.2.1 創建網路

我們需要創建一個網路,將MySQL、Canal、MQ放到同一個Docker網路中:

docker network create heima

讓mysql加入這個網路:

docker network connect heima mysql
2.2.2 安裝Canal

課前資料中提供了canal的鏡像壓縮包:

image

大家可以上傳到虛擬機,然后通過命令匯入:

docker load -i canal.tar

然后運行命令創建Canal容器:

docker run -p 11111:11111 --name canal \
-e canal.destinations=heima \
-e canal.instance.master.address=mysql:3306  \
-e canal.instance.dbUsername=canal  \
-e canal.instance.dbPassword=canal  \
-e canal.instance.connectionCharset=UTF-8 \
-e canal.instance.tsdb.enable=true \
-e canal.instance.gtidon=false  \
-e canal.instance.filter.regex=heima\\..* \
--network heima \
-d canal/canal-server:v1.1.5

說明:

  • -p 11111:11111:這是canal的默認監聽埠
  • -e canal.destinations=heima:這是該canal服務容器的名字(后面要根據名字和java客戶端系結)
  • -e canal.instance.master.address=mysql:3306:資料庫地址和埠,如果不知道mysql容器地址,可以通過docker inspect 容器id來查看
  • -e canal.instance.dbUsername=canal:資料庫用戶名
  • -e canal.instance.dbPassword=canal :資料庫密碼
  • -e canal.instance.filter.regex=:要監聽的表名稱

表名稱監聽支持的語法:

mysql 資料決議關注的表,Perl正則運算式.
多個正則之間以逗號(,)分隔,轉義符需要雙斜杠(\\) 
常見例子:
1.  所有表:.*   or  .*\\..*
2.  canal schema下所有表: canal\\..*
3.  canal下的以canal打頭的表:canal\\.canal.*
4.  canal schema下的一張表:canal.test1
5.  多個規則組合使用然后以逗號隔開:canal\\..*,mysql.test1,mysql.test2 

3. 監聽Canal

Canal提供了各種語言的客戶端,當Canal監聽到binlog變化時,會通知Canal的客戶端,

image

我們可以利用Canal提供的Java客戶端,監聽Canal通知訊息,當收到變化的訊息時,完成對快取的更新,

不過這里我們會使用GitHub上的第三方開源的canal-starter客戶端,地址:https://github.com/NormanGyllenhaal/canal-client

與SpringBoot完美整合,自動裝配,比官方客戶端要簡單好用很多,

3.1 引入依賴:

<dependency>
    <groupId>top.javatool</groupId>
    <artifactId>canal-spring-boot-starter</artifactId>
    <version>1.2.1-RELEASE</version>
</dependency>

3.2 撰寫配置:

canal:
  destination: heima # canal的集群名字,要與安裝canal時設定的名稱一致
  server: 192.168.150.101:11111 # canal服務地址

3.3 修改Item物體類

通過@Id、@Column、等注解完成Item與資料庫表欄位的映射:

package com.heima.item.pojo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Transient;

import javax.persistence.Column;
import java.util.Date;

@Data
@TableName("tb_item")
public class Item {
    @TableId(type = IdType.AUTO)
    @Id
    private Long id;//商品id
    @Column(name = "name")
    private String name;//商品名稱
    private String title;//商品標題
    private Long price;//價格(分)
    private String image;//商品圖片
    private String category;//分類名稱
    private String brand;//品牌名稱
    private String spec;//規格
    private Integer status;//商品狀態 1-正常,2-下架
    private Date createTime;//創建時間
    private Date updateTime;//更新時間
    @TableField(exist = false)
    @Transient
    private Integer stock;
    @TableField(exist = false)
    @Transient
    private Integer sold;
}

3.4 撰寫監聽器

通過實作EntryHandler<T>介面撰寫監聽器,監聽Canal訊息,注意兩點:

  • 實作類通過@CanalTable("tb_item")指定監聽的表資訊
  • EntryHandler的泛型是與表對應的物體類
package com.heima.item.canal;

import com.github.benmanes.caffeine.cache.Cache;
import com.heima.item.config.RedisHandler;
import com.heima.item.pojo.Item;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import top.javatool.canal.client.annotation.CanalTable;
import top.javatool.canal.client.handler.EntryHandler;

@CanalTable("tb_item")
@Component
public class ItemHandler implements EntryHandler<Item> {

    @Autowired
    private RedisHandler redisHandler;
    @Autowired
    private Cache<Long, Item> itemCache;

    @Override
    public void insert(Item item) {
        // 寫資料到JVM行程快取
        itemCache.put(item.getId(), item);
        // 寫資料到redis
        redisHandler.saveItem(item);
    }

    @Override
    public void update(Item before, Item after) {
        // 寫資料到JVM行程快取
        itemCache.put(after.getId(), after);
        // 寫資料到redis
        redisHandler.saveItem(after);
    }

    @Override
    public void delete(Item item) {
        // 洗掉資料到JVM行程快取
        itemCache.invalidate(item.getId());
        // 洗掉資料到redis
        redisHandler.deleteItemById(item.getId());
    }
}

3.5 撰寫業務層

這里面的代碼可以寫到撰寫監聽器里

在這里對Redis的操作都封裝到了RedisHandler這個物件中,是我們之前做快取預熱時撰寫的一個類,內容如下:

package com.heima.item.config;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.heima.item.pojo.Item;
import com.heima.item.pojo.ItemStock;
import com.heima.item.service.IItemService;
import com.heima.item.service.IItemStockService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class RedisHandler{

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private IItemService itemService;
    @Autowired
    private IItemStockService stockService;

    private static final ObjectMapper MAPPER = new ObjectMapper();


    public void saveItem(Item item) {
        try {
            String json = MAPPER.writeValueAsString(item);
            redisTemplate.opsForValue().set("item:id:" + item.getId(), json);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

    public void deleteItemById(Long id) {
        redisTemplate.delete("item:id:" + id);
    }
}

本文來自博客園,作者:不吃紫菜,遵循CC 4.0 BY-SA著作權協議,

轉載請附上原文出處鏈接:https://www.cnblogs.com/buchizicai/p/17093768.html及本宣告;

本文著作權歸作者所有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/543956.html

標籤:Java

上一篇:時隔多年,這次我終于把動態代理的原始碼翻了個地兒朝天

下一篇:Gateway-服務網關

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more