主頁 > 後端開發 > 12306 的架構也太 牛X 了吧!

12306 的架構也太 牛X 了吧!

2020-09-22 09:30:41 後端開發

作者:繪你一世傾城
juejin.im/post/5d84e21f6fb9a06ac8248149

每到節假日期間,一二線城市返鄉、外出游玩的人們幾乎都面臨著一個問題:搶火車票!

雖然現在大多數情況下都能訂到票,但是放票瞬間即無票的場景,相信大家都深有體會,尤其是春節期間,大家不僅使用12306,還會考慮“智行”和其他的搶票軟體,全國上下幾億人在這段時間都在搶票,

“12306服務”承受著這個世界上任何秒殺系統都無法超越的QPS,上百萬的并發再正常不過了!筆者專門研究了一下“12306”的服務端架構,學習到了其系統設計上很多亮點,

在這里和大家分享一下并模擬一個例子:如何在100萬人同時搶1萬張火車票時,系統提供正常、穩定的服務,

GitGub代碼地址:https://github.com/GuoZhaoran/spikeSystem

1、大型高并發系統架構

高并發的系統架構都會采用分布式集群部署,服務上層有著層層負載均衡,并提供各種容災手段(雙火機房、節點容錯、服務器災備等)保證系統的高可用,流量也會根據不同的負載能力和配置策略均衡到不同的服務器上,下邊是一個簡單的示意圖:

1.1 負載均衡簡介

上圖中描述了用戶請求到服務器經歷了三層的負載均衡,下邊分別簡單介紹一下這三種負載均衡:

OSPF(開放式最短鏈路優先)是一個內部網關協議(Interior Gateway Protocol,簡稱IGP),OSPF通過路由器之間通告網路介面的狀態來建立鏈路狀態資料庫,生成最短路徑樹,OSPF會自動計算路由介面上的Cost值,但也可以通過手工指定該介面的Cost值,手工指定的優先于自動計算的值,

OSPF計算的Cost,同樣是和介面帶寬成反比,帶寬越高,Cost值越小,到達目標相同Cost值的路徑,可以執行負載均衡,最多6條鏈路同時執行負載均衡,

LVS (Linux VirtualServer),它是一種集群(Cluster)技術,采用IP負載均衡技術和基于內容請求分發技術,調度器具有很好的吞吐率,將請求均衡地轉移到不同的服務器上執行,且調度器自動屏蔽掉服務器的故障,從而將一組服務器構成一個高性能的、高可用的虛擬服務器,

Nginx想必大家都很熟悉了,是一款非常高性能的http代理/反向代理服務器,服務開發中也經常使用它來做負載均衡,推薦大家看這篇:超詳細 Nginx 極簡教程,傻瓜一看也會,

Nginx實作負載均衡的方式主要有三種:輪詢、加權輪詢、ip hash輪詢,下面我們就針對Nginx的加權輪詢做專門的配置和測驗

1.2 Nginx加權輪詢的演示

Nginx實作負載均衡通過upstream模塊實作,其中加權輪詢的配置是可以給相關的服務加上一個權重值,配置的時候可能根據服務器的性能、負載能力設定相應的負載,下面是一個加權輪詢負載的配置,我將在本地的監聽3001-3004埠,分別配置1,2,3,4的權重:

#配置負載均衡
upstream load_rule {
   server 127.0.0.1:3001 weight=1;
   server 127.0.0.1:3002 weight=2;
   server 127.0.0.1:3003 weight=3;
   server 127.0.0.1:3004 weight=4;
}
...
server {
listen 80;
server_name load_balance.com www.load_balance.com;
location / {
   proxy_pass http://load_rule;
}
}

我在本地/etc/hosts目錄下配置了 www.load_balance.com的虛擬域名地址,接下來使用Go語言開啟四個http埠監聽服務,下面是監聽在3001埠的Go程式,其他幾個只需要修改埠即可:

package main

import (
    "net/http"
    "os"
    "strings"
)

func main() {
    http.HandleFunc("/buy/ticket", handleReq)
    http.ListenAndServe(":3001", nil)
}

//處理請求函式,根據請求將回應結果資訊寫入日志
func handleReq(w http.ResponseWriter, r *http.Request) {
    failedMsg := "handle in port:"
    writeLog(failedMsg, "./stat.log")
}

//寫入日志
func writeLog(msg string, logPath string) {
    fd, _ := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
    defer fd.Close()
    content := strings.Join([]string{msg, "\r\n"}, "3001")
    buf := []byte(content)
    fd.Write(buf)
}

我將請求的埠日志資訊寫到了./stat.log檔案當中,然后使用ab壓測工具做壓測:

ab -n 1000 -c 100 http://www.load_balance.com/buy/ticket

統計日志中的結果,3001-3004埠分別得到了100、200、300、400的請求量,這和我在nginx中配置的權重占比很好的吻合在了一起,并且負載后的流量非常的均勻、隨機,

具體的實作大家可以參考nginx的upsteam模塊實作原始碼,這里推薦一篇文章:

https://www.kancloud.cn/digest/understandingnginx/202607

2、秒殺搶購系統選型

回到我們最初提到的問題中來:火車票秒殺系統如何在高并發情況下提供正常、穩定的服務呢?

從上面的介紹我們知道用戶秒殺流量通過層層的負載均衡,均勻到了不同的服務器上,即使如此,集群中的單機所承受的QPS也是非常高的,如何將單機性能優化到極致呢?要解決這個問題,我們就要想明白一件事:

通常訂票系統要處理生成訂單、減扣庫存、用戶支付這三個基本的階段,我們系統要做的事情是要保證火車票訂單不超賣、不少賣,每張售賣的車票都必須支付才有效,還要保證系統承受極高的并發,

這三個階段的先后順序改怎么分配才更加合理呢?我們來分析一下:

2.1 下單減庫存

當用戶并發請求到達服務端時,首先創建訂單,然后扣除庫存,等待用戶支付,這種順序是我們一般人首先會想到的解決方案,這種情況下也能保證訂單不會超賣,因為創建訂單之后就會減庫存,這是一個原子操作,

但是這樣也會產生一些問題,

第一就是在極限并發情況下,任何一個記憶體操作的細節都至關影響性能,尤其像創建訂單這種邏輯,一般都需要存盤到磁盤資料庫的,對資料庫的壓力是可想而知的;

第二是如果用戶存在惡意下單的情況,只下單不支付這樣庫存就會變少,會少賣很多訂單,雖然服務端可以限制IP和用戶的購買訂單數量,這也不算是一個好方法,

關注微信公眾號:Java技術堆疊,在后臺回復:架構,可以獲取我整理的 N 篇最新架構教程,都是干貨,

2.2 支付減庫存

如果等待用戶支付了訂單在減庫存,第一感覺就是不會少賣,但是這是并發架構的大忌,因為在極限并發情況下,用戶可能會創建很多訂單,當庫存減為零的時候很多用戶發現搶到的訂單支付不了了,這也就是所謂的“超賣”,也不能避免并發操作資料庫磁盤IO

2.3 預扣庫存

從上邊兩種方案的考慮,我們可以得出結論:只要創建訂單,就要頻繁操作資料庫IO,那么有沒有一種不需要直接操作資料庫IO的方案呢,這就是預扣庫存,先扣除了庫存,保證不超賣,然后異步生成用戶訂單,這樣回應給用戶的速度就會快很多;那么怎么保證不少賣呢?

用戶拿到了訂單,不支付怎么辦?我們都知道現在訂單都有有效期,比如說用戶五分鐘內不支付,訂單就失效了,訂單一旦失效,就會加入新的庫存,這也是現在很多網上零售企業保證商品不少賣采用的方案,訂單的生成是異步的,一般都會放到MQ、kafka這樣的即時消費佇列中處理,訂單量比較少的情況下,生成訂單非常快,用戶幾乎不用排隊,

3、扣庫存的藝術

從上面的分析可知,顯然預扣庫存的方案最合理,我們進一步分析扣庫存的細節,這里還有很大的優化空間,庫存存在哪里?怎樣保證高并發下,正確的扣庫存,還能快速的回應用戶請求?

在單機低并發情況下,我們實作扣庫存通常是這樣的:

為了保證扣庫存和生成訂單的原子性,需要采用事務處理,然后取庫存判斷、減庫存,最后提交事務,整個流程有很多IO,對資料庫的操作又是阻塞的,這種方式根本不適合高并發的秒殺系統,

接下來我們對單機扣庫存的方案做優化:本地扣庫存,我們把一定的庫存量分配到本地機器,直接在記憶體中減庫存,然后按照之前的邏輯異步創建訂單,改進過之后的單機系統是這樣的:

這樣就避免了對資料庫頻繁的IO操作,只在記憶體中做運算,極大的提高了單機抗并發的能力,但是百萬的用戶請求量單機是無論如何也抗不住的,雖然nginx處理網路請求使用epoll模型,c10k的問題在業界早已得到了解決,

但是linux系統下,一切資源皆檔案,網路請求也是這樣,大量的檔案描述符會使作業系統瞬間失去回應,

上面我們提到了nginx的加權均衡策略,我們不妨假設將100W的用戶請求量平均均衡到100臺服務器上,這樣單機所承受的并發量就小了很多,然后我們每臺機器本地庫存100張火車票,100臺服務器上的總庫存還是1萬,這樣保證了庫存訂單不超賣,下面是我們描述的集群架構:

問題接踵而至,在高并發情況下,現在我們還無法保證系統的高可用,假如這100臺服務器上有兩三臺機器因為扛不住并發的流量或者其他的原因宕機了,那么這些服務器上的訂單就賣不出去了,這就造成了訂單的少賣,

要解決這個問題,我們需要對總訂單量做統一的管理,這就是接下來的容錯方案,服務器不僅要在本地減庫存,另外要遠程統一減庫存,有了遠程統一減庫存的操作,我們就可以根據機器負載情況,為每臺機器分配一些多余的“buffer庫存”用來防止機器中有機器宕機的情況,

我們結合下面架構圖具體分析一下:

我們采用Redis存盤統一庫存,因為Redis的性能非常高,號稱單機QPS能抗10W的并發,在本地減庫存以后,如果本地有訂單,我們再去請求redis遠程減庫存,本地減庫存和遠程減庫存都成功了,才回傳給用戶搶票成功的提示,這樣也能有效的保證訂單不會超賣,推薦閱讀:掌握這些 Redis 技巧,百億資料量不在話下,

當機器中有機器宕機時,因為每個機器上有預留的buffer余票,所以宕機機器上的余票依然能夠在其他機器上得到彌補,保證了不少賣,

buffer余票設定多少合適呢,理論上buffer設定的越多,系統容忍宕機的機器數量就越多,但是buffer設定的太大也會對redis造成一定的影響,

雖然redis記憶體資料庫抗并發能力非常高,請求依然會走一次網路IO,其實搶票程序中對redis的請求次數是本地庫存和buffer庫存的總量,因為當本地庫存不足時,系統直接回傳用戶“已售罄”的資訊提示,就不會再走統一扣庫存的邏輯,這在一定程度上也避免了巨大的網路請求量把redis壓跨,所以buffer值設定多少,需要架構師對系統的負載能力做認真的考量,

4、代碼演示

Go語言原生為并發設計,我采用go語言給大家演示一下單機搶票的具體流程,


4.1 初始化作業

go包中的init函式先于main函式執行,在這個階段主要做一些準備性作業,我們系統需要做的準備作業有:初始化本地庫存、初始化遠程redis存盤統一庫存的hash鍵值、初始化redis連接池;

另外還需要初始化一個大小為1的int型別chan,目的是實作分布式鎖的功能,也可以直接使用讀寫鎖或者使用redis等其他的方式避免資源競爭,但使用channel更加高效,這就是go語言的哲學:不要通過共享記憶體來通信,而要通過通信來共享記憶體,redis庫使用的是redigo,下面是代碼實作:

...
//localSpike包結構體定義
package localSpike

type LocalSpike struct {
    LocalInStock int64
    LocalSalesVolume int64
}
...
//remoteSpike對hash結構的定義和redis連接池
package remoteSpike
//遠程訂單存盤健值
type RemoteSpikeKeys struct {
    SpikeOrderHashKey string    //redis中秒殺訂單hash結構key
    TotalInventoryKey string    //hash結構中總訂單庫存key
    QuantityOfOrderKey string   //hash結構中已有訂單數量key
}

//初始化redis連接池
func NewPool() *redis.Pool {
    return &redis.Pool{
        MaxIdle: 10000,
        MaxActive: 12000, // max number of connections
        Dial: func() (redis.Conn, error) {
            c, err := redis.Dial("tcp", ":6379")
            if err != nil {
                panic(err.Error())
            }
            return c, err
        },
    }
}
...
func init() {
    localSpike = localSpike2.LocalSpike{
        LocalInStock: 150,
        LocalSalesVolume: 0,
    }
    remoteSpike = remoteSpike2.RemoteSpikeKeys{
        SpikeOrderHashKey: "ticket_hash_key",
        TotalInventoryKey: "ticket_total_nums",
        QuantityOfOrderKey: "ticket_sold_nums",
    }
    redisPool = remoteSpike2.NewPool()
    done = make(chan int, 1)
    done <- 1
}

4.2 本地扣庫存和統一扣庫存

本地扣庫存邏輯非常簡單,用戶請求過來,添加銷量,然后對比銷量是否大于本地庫存,回傳bool值:

package localSpike
//本地扣庫存,回傳bool值
func (spike *LocalSpike) LocalDeductionStock() bool{
    spike.LocalSalesVolume = spike.LocalSalesVolume + 1
    return spike.LocalSalesVolume < spike.LocalInStock
}

注意這里對共享資料LocalSalesVolume的操作是要使用鎖來實作的,但是因為本地扣庫存和統一扣庫存是一個原子性操作,所以在最上層使用channel來實作,這塊后邊會講,

統一扣庫存操作redis,因為redis是單執行緒的,而我們要實作從中取資料,寫資料并計算一些列步驟,我們要配合lua腳本打包命令,保證操作的原子性:

package remoteSpike
......
const LuaScript = `
        local ticket_key = KEYS[1]
        local ticket_total_key = ARGV[1]
        local ticket_sold_key = ARGV[2]
        local ticket_total_nums = tonumber(redis.call('HGET', ticket_key, ticket_total_key))
        local ticket_sold_nums = tonumber(redis.call('HGET', ticket_key, ticket_sold_key))
        -- 查看是否還有余票,增加訂單數量,回傳結果值
       if(ticket_total_nums >= ticket_sold_nums) then
            return redis.call('HINCRBY', ticket_key, ticket_sold_key, 1)
        end
        return 0
`
//遠端統一扣庫存
func (RemoteSpikeKeys *RemoteSpikeKeys) RemoteDeductionStock(conn redis.Conn) bool {
    lua := redis.NewScript(1, LuaScript)
    result, err := redis.Int(lua.Do(conn, RemoteSpikeKeys.SpikeOrderHashKey, RemoteSpikeKeys.TotalInventoryKey, RemoteSpikeKeys.QuantityOfOrderKey))
    if err != nil {
        return false
    }
    return result != 0
}

我們使用hash結構存盤總庫存和總銷量的資訊,用戶請求過來時,判斷總銷量是否大于庫存,然后回傳相關的bool值,在啟動服務之前,我們需要初始化redis的初始庫存資訊:

hmset ticket_hash_key "ticket_total_nums" 10000 "ticket_sold_nums" 0

4.3 回應用戶資訊

我們開啟一個http服務,監聽在一個埠上:

package main
...
func main() {
    http.HandleFunc("/buy/ticket", handleReq)
    http.ListenAndServe(":3005", nil)
}

上面我們做完了所有的初始化作業,接下來handleReq的邏輯非常清晰,判斷是否搶票成功,回傳給用戶資訊就可以了,

package main
//處理請求函式,根據請求將回應結果資訊寫入日志
func handleReq(w http.ResponseWriter, r *http.Request) {
    redisConn := redisPool.Get()
    LogMsg := ""
    <-done
    //全域讀寫鎖
    if localSpike.LocalDeductionStock() && remoteSpike.RemoteDeductionStock(redisConn) {
        util.RespJson(w, 1, "搶票成功", nil)
        LogMsg = LogMsg + "result:1,localSales:" + strconv.FormatInt(localSpike.LocalSalesVolume, 10)
    } else {
        util.RespJson(w, -1, "已售罄", nil)
        LogMsg = LogMsg + "result:0,localSales:" + strconv.FormatInt(localSpike.LocalSalesVolume, 10)
    }
    done <- 1

    //將搶票狀態寫入到log中
    writeLog(LogMsg, "./stat.log")
}

func writeLog(msg string, logPath string) {
    fd, _ := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
    defer fd.Close()
    content := strings.Join([]string{msg, "\r\n"}, "")
    buf := []byte(content)
    fd.Write(buf)
}

前邊提到我們扣庫存時要考慮競態條件,我們這里是使用channel避免并發的讀寫,保證了請求的高效順序執行,我們將介面的回傳資訊寫入到了./stat.log檔案方便做壓測統計,

4.4 單機服務壓測

開啟服務,我們使用ab壓測工具進行測驗:

ab -n 10000 -c 100 http://127.0.0.1:3005/buy/ticket

下面是我本地低配mac的壓測資訊:

This is ApacheBench, Version 2.3 <$Revision: 1826891 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 127.0.0.1 (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Completed 10000 requests
Finished 10000 requests

Server Software:
Server Hostname: 127.0.0.1
Server Port:            3005

Document Path: /buy/ticket
Document Length: 29 bytes

Concurrency Level:      100
Time taken for tests:   2.339 seconds
Complete requests:      10000
Failed requests:        0
Total transferred: 1370000 bytes
HTML transferred: 290000 bytes
Requests per second: 4275.96 [#/sec] (mean)
Time per request:       23.387 [ms] (mean)
Time per request:       0.234 [ms] (mean, across all concurrent requests)
Transfer rate: 572.08 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median max
Connect:        0    8  14.7      6     223
Processing:     2   15  17.6     11     232
Waiting:        1   11  13.5      8     225
Total:          7   23  22.8     18     239

Percentage of the requests served within a certain time (ms)
  50% 18
  66% 24
  75% 26
  80% 28
  90% 33
  95% 39
  98% 45
  99% 54
 100% 239 (longest request)

根據指標顯示,我單機每秒就能處理4000+的請求,正常服務器都是多核配置,處理1W+的請求根本沒有問題,而且查看日志發現整個服務程序中,請求都很正常,流量均勻,redis也很正常:

//stat.log
...
result:1,localSales:145
result:1,localSales:146
result:1,localSales:147
result:1,localSales:148
result:1,localSales:149
result:1,localSales:150
result:0,localSales:151
result:0,localSales:152
result:0,localSales:153
result:0,localSales:154
result:0,localSales:156
...

5、總結回顧

總體來說,秒殺系統是非常復雜的,我們這里只是簡單介紹模擬了一下單機如何優化到高性能,集群如何避免單點故障,保證訂單不超賣、不少賣的一些策略,完整的訂單系統還有訂單進度的查看,每臺服務器上都有一個任務,定時的從總庫存同步余票和庫存資訊展示給用戶,還有用戶在訂單有效期內不支付,釋放訂單,補充到庫存等等,

我們實作了高并發搶票的核心邏輯,可以說系統設計的非常的巧妙,巧妙的避開了對DB資料庫IO的操作,對Redis網路IO的高并發請求,幾乎所有的計算都是在記憶體中完成的,而且有效的保證了不超賣、不少賣,還能夠容忍部分機器的宕機,我覺得其中有兩點特別值得學習總結:

負載均衡,分而治之,通過負載均衡,將不同的流量劃分到不同的機器上,每臺機器處理好自己的請求,將自己的性能發揮到極致,這樣系統的整體也就能承受極高的并發了,就像作業的的一個團隊,每個人都將自己的價值發揮到了極致,團隊成長自然是很大的,

合理的使用并發和異步,自epoll網路架構模型解決了c10k問題以來,異步越來被服務端開發人員所接受,能夠用異步來做的作業,就用異步來做,在功能拆解上能達到意想不到的效果,這點在nginx、node.js、redis上都能體現,他們處理網路請求使用的epoll模型,用實踐告訴了我們單執行緒依然可以發揮強大的威力,

服務器已經進入了多核時代,go語言這種天生為并發而生的語言,完美的發揮了服務器多核優勢,很多可以并發處理的任務都可以使用并發來解決,比如go處理http請求時每個請求都會在一個goroutine中執行,總之:怎樣合理的壓榨CPU,讓其發揮出應有的價值,是我們一直需要探索學習的方向,

關注公眾號Java技術堆疊回復"面試"獲取我整理的2020最全面試題及答案,

推薦去我的博客閱讀更多:

1.Java JVM、集合、多執行緒、新特性系列教程

2.Spring MVC、Spring Boot、Spring Cloud 系列教程

3.Maven、Git、Eclipse、Intellij IDEA 系列工具教程

4.Java、后端、架構、阿里巴巴等大廠最新面試題

覺得不錯,別忘了點贊+轉發哦!

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

標籤:Java

上一篇:Java 在 PDF 中添加表單域

下一篇:小技巧整理(一)

標籤雲
其他(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