Redis 記憶體突增時,如何定量分析其記憶體使用情況

来源:https://www.cnblogs.com/ivictor/p/18426260
-Advertisement-
Play Games

背景 最近碰到一個 case,一個 Redis 實例的記憶體突增,used_memory最大時達到了 78.9G,而該實例的maxmemory配置卻只有 16G,最終導致實例中的數據被大量驅逐。 以下是問題發生時INFO MEMORY的部分輸出內容。 # Memoryused_memory:84716 ...


背景

最近碰到一個 case,一個 Redis 實例的記憶體突增,used_memory最大時達到了 78.9G,而該實例的maxmemory配置卻只有 16G,最終導致實例中的數據被大量驅逐。

以下是問題發生時INFO MEMORY的部分輸出內容。

# Memory
used_memory:84716542624
used_memory_human:78.90G
used_memory_rss:104497676288
used_memory_rss_human:97.32G
used_memory_peak:84716542624
used_memory_peak_human:78.90G
used_memory_peak_perc:100.00%
used_memory_overhead:75682545624
used_memory_startup:906952
used_memory_dataset:9033997000
used_memory_dataset_perc:10.66%
allocator_allocated:84715102264
allocator_active:101370822656
allocator_resident:102303637504
total_system_memory:810745470976
total_system_memory_human:755.07G
used_memory_lua:142336
used_memory_lua_human:139.00K
used_memory_scripts:6576
used_memory_scripts_human:6.42K
number_of_cached_scripts:13
maxmemory:17179869184
maxmemory_human:16.00G
maxmemory_policy:volatile-lru
allocator_frag_ratio:1.20
allocator_frag_bytes:16655720392

記憶體突增導致數據被驅逐,是 Redis 中一個較為常見的問題。很多童鞋在面對這類問題時往往缺乏清晰的分析思路,常常誤以為是複製、RDB 持久化等操作引起的。接下來,我們看看如何系統地分析這類問題。

本文主要包括以下幾部分:

  1. INFO 中的used_memory是怎麼來的?
  2. 什麼是used_memory
  3. used_memory記憶體通常會被用於哪些場景?
  4. Redis 7 在記憶體統計方面的變化。
  5. 數據驅逐的觸發條件——當used_memory 超過maxmemory後,是否一定會觸發驅逐?
  6. 最後,分享一個腳本,幫助實時分析used_memory增長時,具體是哪一部分的記憶體消耗導致的。

INFO 中的 used_memory 是怎麼來的?

當我們執行INFO命令時,Redis 會調用genRedisInfoString函數來生成其輸出。

// server.c
sds genRedisInfoString(const char *section) {
    ...
    /* Memory */
    if (allsections || defsections || !strcasecmp(section,"memory")) {
        ...
        size_t zmalloc_used = zmalloc_used_memory();
        ...
        if (sections++) info = sdscat(info,"\r\n");
        info = sdscatprintf(info,
            "# Memory\r\n"
            "used_memory:%zu\r\n"
            "used_memory_human:%s\r\n"
            "used_memory_rss:%zu\r\n"
            ...
            "lazyfreed_objects:%zu\r\n",
            zmalloc_used,
            hmem,
            server.cron_malloc_stats.process_rss,
            ...
            lazyfreeGetFreedObjectsCount()
        );
        freeMemoryOverheadData(mh);
    }
    ...
    return info;
}

可以看到,used_memory 的值來自 zmalloc_used,而 zmalloc_used 又是通過zmalloc_used_memory()函數獲取的。

// zmalloc.c
size_t zmalloc_used_memory(void) {
    size_t um;
    atomicGet(used_memory,um);
    return um;
}

zmalloc_used_memory() 的實現很簡單,就是以原子方式讀取 used_memory 的值。

什麼是 used_memory

used_memory是一個靜態變數,其類型為redisAtomic size_t,其中redisAtomic_Atomic類型的別名。_Atomic是 C11 標準引入的關鍵字,用於聲明原子類型,保證在多線程環境中對該類型的操作是原子的,避免數據競爭。

#define redisAtomic _Atomic
static redisAtomic size_t used_memory = 0;

used_memory 的更新主要通過兩個巨集定義實現:

#define update_zmalloc_stat_alloc(__n) atomicIncr(used_memory,(__n))
#define update_zmalloc_stat_free(__n) atomicDecr(used_memory,(__n))

其中,update_zmalloc_stat_alloc(__n)是在分配記憶體時調用,它通過原子操作讓 used_memory 加__n。

update_zmalloc_stat_free(__n)則是在釋放記憶體時調用,它通過原子操作讓 used_memory 減__n

這兩個巨集確保了在記憶體分配和釋放過程中used_memory的準確更新,並且避免了併發操作帶來的數據競爭問題。

在通過記憶體分配器(常用的記憶體分配器有 glibc's malloc、jemalloc、tcmalloc,Redis 中一般使用 jemalloc)中的函數分配或釋放記憶體時,會同步調用update_zmalloc_stat_allocupdate_zmalloc_stat_free來更新 used_memory 的值。

在 Redis 中,記憶體管理主要通過以下兩個函數來實現:

// zmalloc.c
void *ztrymalloc_usable(size_t size, size_t *usable) {
    ASSERT_NO_SIZE_OVERFLOW(size);
    void *ptr = malloc(MALLOC_MIN_SIZE(size)+PREFIX_SIZE);

    if (!ptr) return NULL;
#ifdef HAVE_MALLOC_SIZE
    size = zmalloc_size(ptr);
    update_zmalloc_stat_alloc(size);
    if (usable) *usable = size;
    return ptr;
#else
    ...
#endif
}

void zfree(void *ptr) {
    ...
    if (ptr == NULL) return;
#ifdef HAVE_MALLOC_SIZE
    update_zmalloc_stat_free(zmalloc_size(ptr));
    free(ptr);
#else
   ...
#endif
}

其中,

  • ztrymalloc_usable函數用於分配記憶體。該函數首先會調用malloc分配記憶體。如果分配成功,則會通過 update_zmalloc_stat_alloc更新 used_memory 的值。
  • zfree 函數用於釋放記憶體。在釋放記憶體之前,先通過update_zmalloc_stat_free調整 used_memory 的值,然後再調用free釋放記憶體。

這種機制保證了 Redis 能夠準確跟蹤記憶體的分配和釋放情況,從而有效地管理記憶體使用。

used_memory 記憶體通常會被用於哪些場景?

used_memory主要由兩部分組成:

  1. 數據本身:對應 INFO 中的used_memory_dataset
  2. 內部管理和維護數據結構的開銷:對應 INFO 中的used_memory_overhead

需要註意的是,used_memory_dataset 並不是根據 Key 的數量及 Key 使用的記憶體計算出來的,而是通過 used_memory 減去 used_memory_overhead 得到的。

接下來,我們重點分析下used_memory_overhead 的來源。實際上,Redis 提供了一個單獨的函數-getMemoryOverheadData,專門用於計算這一部分的記憶體開銷。

// object.c
struct redisMemOverhead *getMemoryOverheadData(void) {
    int j;
    // mem_total 用於累積總的記憶體開銷,最後會賦值給 used_memory_overhead。
    size_t mem_total = 0;
    // mem 用於計算每一部分的記憶體使用量。
    size_t mem = 0;
    // 調用 zmalloc_used_memory() 獲取 used_memory。
    size_t zmalloc_used = zmalloc_used_memory();
    // 使用 zcalloc 分配一個 redisMemOverhead 結構體的記憶體。
    struct redisMemOverhead *mh = zcalloc(sizeof(*mh));
    ...
    // 將 Redis 啟動時的記憶體使用量 server.initial_memory_usage 加入到總記憶體開銷中。
    mem_total += server.initial_memory_usage;

    mem = 0;
    // 將複製積壓緩衝區的記憶體開銷加入到總記憶體開銷中。
    if (server.repl_backlog)
        mem += zmalloc_size(server.repl_backlog);
    mh->repl_backlog = mem;
    mem_total += mem;

    /* Computing the memory used by the clients would be O(N) if done
     * here online. We use our values computed incrementally by
     * clientsCronTrackClientsMemUsage(). */
    // 計算客戶端記憶體開銷
    mh->clients_slaves = server.stat_clients_type_memory[CLIENT_TYPE_SLAVE];
    mh->clients_normal = server.stat_clients_type_memory[CLIENT_TYPE_MASTER]+
                         server.stat_clients_type_memory[CLIENT_TYPE_PUBSUB]+
                         server.stat_clients_type_memory[CLIENT_TYPE_NORMAL];
    mem_total += mh->clients_slaves;
    mem_total += mh->clients_normal;
    // 計算 AOF 緩衝區和 AOF Rewrite Buffer 的記憶體開銷
    mem = 0;
    if (server.aof_state != AOF_OFF) {
        mem += sdsZmallocSize(server.aof_buf);
        mem += aofRewriteBufferSize();
    }
    mh->aof_buffer = mem;
    mem_total+=mem;
    // 計算 Lua 腳本緩存的記憶體開銷
    mem = server.lua_scripts_mem;
    mem += dictSize(server.lua_scripts) * sizeof(dictEntry) +
        dictSlots(server.lua_scripts) * sizeof(dictEntry*);
    mem += dictSize(server.repl_scriptcache_dict) * sizeof(dictEntry) +
        dictSlots(server.repl_scriptcache_dict) * sizeof(dictEntry*);
    if (listLength(server.repl_scriptcache_fifo) > 0) {
        mem += listLength(server.repl_scriptcache_fifo) * (sizeof(listNode) +
            sdsZmallocSize(listNodeValue(listFirst(server.repl_scriptcache_fifo))));
    }
    mh->lua_caches = mem;
    mem_total+=mem;
    // 計算資料庫的記憶體開銷:遍歷所有資料庫 (server.dbnum)。對於每個資料庫,計算主字典 (db->dict) 和過期字典 (db->expires) 的記憶體開銷。
    for (j = 0; j < server.dbnum; j++) {
        redisDb *db = server.db+j;
        long long keyscount = dictSize(db->dict);
        if (keyscount==0) continue;

        mh->total_keys += keyscount;
        mh->db = zrealloc(mh->db,sizeof(mh->db[0])*(mh->num_dbs+1));
        mh->db[mh->num_dbs].dbid = j;

        mem = dictSize(db->dict) * sizeof(dictEntry) +
              dictSlots(db->dict) * sizeof(dictEntry*) +
              dictSize(db->dict) * sizeof(robj);
        mh->db[mh->num_dbs].overhead_ht_main = mem;
        mem_total+=mem;

        mem = dictSize(db->expires) * sizeof(dictEntry) +
              dictSlots(db->expires) * sizeof(dictEntry*);
        mh->db[mh->num_dbs].overhead_ht_expires = mem;
        mem_total+=mem;

        mh->num_dbs++;
    }
    // 將計算的 mem_total 賦值給 mh->overhead_total。
    mh->overhead_total = mem_total;
    // 計算數據的記憶體開銷 (zmalloc_used - mem_total) 並存儲在 mh->dataset。
    mh->dataset = zmalloc_used - mem_total;
    mh->peak_perc = (float)zmalloc_used*100/mh->peak_allocated;

    /* Metrics computed after subtracting the startup memory from
     * the total memory. */
    size_t net_usage = 1;
    if (zmalloc_used > mh->startup_allocated)
        net_usage = zmalloc_used - mh->startup_allocated;
    mh->dataset_perc = (float)mh->dataset*100/net_usage;
    mh->bytes_per_key = mh->total_keys ? (net_usage / mh->total_keys) : 0;

    return mh;
}

基於上面代碼的分析,可以知道 used_memory_overhead 由以下幾部分組成:

  • server.initial_memory_usage:Redis 啟動時的記憶體使用量,對應 INFO 中used_memory_startup

  • mh->repl_backlog:複製積壓緩衝區的記憶體開銷,對應 INFO 中的mem_replication_backlog

  • mh->clients_slaves:從庫的記憶體開銷。對應 INFO 中的mem_clients_slaves

  • mh->clients_normal:其它客戶端的記憶體開銷,對應 INFO 中的mem_clients_normal

  • mh->aof_buffer:AOF 緩衝區和 AOF 重寫緩衝區的記憶體開銷,對應 INFO 中的mem_aof_buffer。AOF 緩衝區是數據寫入 AOF 之前使用的緩衝區。AOF 重寫緩衝區是 AOF 重寫期間,用於存放新增數據的緩衝區。

  • mh->lua_caches:Lua 腳本緩存的記憶體開銷,對應 INFO 中的used_memory_scripts。Redis 5.0 新增的。

  • 字典的記憶體開銷,這部分記憶體在 INFO 中沒有顯示,需要通過MEMORY STATS查看。

    17) "db.0"
    18) 1) "overhead.hashtable.main"
        2) (integer) 2536870912
        3) "overhead.hashtable.expires"
        4) (integer) 0

在這些記憶體開銷中,used_memory_startup 基本不變,mem_replication_backlog 受 repl-backlog-size 的限制,used_memory_scripts 開銷一般不大,而字典的記憶體開銷則與數據量的大小成正比。

所以,重點需要註意的主要有三項:mem_clients_slavesmem_clients_normal 和mem_aof_buffer

  • mem_aof_buffer:重點關註 AOF 重寫期間緩衝區的大小。
  • mem_clients_slaves 和 mem_clients_normal:都是客戶端,記憶體分配方式相同。客戶端的記憶體開銷主要包括以下三部分:
    1. 輸入緩衝區:用於暫存客戶端命令,大小由 client-query-buffer-limit 限制。
    2. 輸出緩衝區:用於緩存發送給客戶端的數據,大小受 client-output-buffer-limit 控制。如果數據超過軟硬限制並持續一段時間,客戶端會被關閉。
    3. 客戶端對象本身占用的記憶體。

Redis 7 在記憶體統計方面的變化

在 Redis 7 中,還會統計以下項的記憶體開銷:

  • mh->cluster_links:集群鏈接的記憶體開銷,對應 INFO 中的mem_cluster_links
  • mh->functions_caches:Function 緩存的記憶體開銷,對應 INFO 中的used_memory_functions
  • 集群模式下鍵到槽映射的記憶體開銷,對應 MEMORY STATS 中的overhead.hashtable.slot-to-keys

此外,Redis 7 引入了 Multi-Part AOF,這個特性移除了 AOF 重寫緩衝區。

需要註意的是,mh->repl_backlog 和 mh->clients_slaves 的記憶體計算方式也發生了變化。

在 Redis 7 之前,mh->repl_backlog 統計的是複製積壓緩衝區的大小,mh->clients_slaves 統計的是所有從節點客戶端的記憶體使用量。

if (server.repl_backlog)
    mem += zmalloc_size(server.repl_backlog);
mh->repl_backlog = mem;
mem_total += mem;

mem = 0;
// 遍歷所有從節點客戶端,累加它們的輸出緩衝區、輸入緩衝區的記憶體使用量以及客戶端對象本身的記憶體占用。
if (listLength(server.slaves)) {
    listIter li;
    listNode *ln;

    listRewind(server.slaves,&li);
    while((ln = listNext(&li))) {
        client *c = listNodeValue(ln);
        mem += getClientOutputBufferMemoryUsage(c);
        mem += sdsAllocSize(c->querybuf);
        mem += sizeof(client);
    }
}
mh->clients_slaves = mem;

因為每個從節點都會分配一個獨立的複製緩衝區(即從節點對應客戶端的輸出緩衝區),所以當從節點的數量增加時,這種實現方式會造成記憶體的浪費。不僅如此,當client-output-buffer-limit設置過大且從節點數量過多時,還容易導致主節點 OOM。

針對這個問題,Redis 7 引入了一個全局複製緩衝區。無論是複製積壓緩衝區(repl-backlog),還是從節點的複製緩衝區都是共用這個緩衝區。

replBufBlock結構體用於存儲全局複製緩衝區的一個塊。

typedef struct replBufBlock {
    int refcount;           /* Number of replicas or repl backlog using. */
    long long id;           /* The unique incremental number. */
    long long repl_offset;  /* Start replication offset of the block. */
    size_t size, used;
    char buf[];
} replBufBlock;

每個replBufBlock包含一個refcount欄位,用於記錄該塊被多少個複製實例(包括主節點的複製積壓緩衝區和從節點)所引用。

當新的從節點添加時,Redis 不會為其分配新的複製緩衝區塊,而是增加現有replBufBlockrefcount

相應地,在 Redis 7 中,mh->repl_backlog 和 mh->clients_slaves 的記憶體計算方式也發生了變化。

if (listLength(server.slaves) &&
    (long long)server.repl_buffer_mem > server.repl_backlog_size)
{
    mh->clients_slaves = server.repl_buffer_mem - server.repl_backlog_size;
    mh->repl_backlog = server.repl_backlog_size;
} else {
    mh->clients_slaves = 0;
    mh->repl_backlog = server.repl_buffer_mem;
}
if (server.repl_backlog) {
    /* The approximate memory of rax tree for indexed blocks. */
    mh->repl_backlog +=
        server.repl_backlog->blocks_index->numnodes * sizeof(raxNode) +
        raxSize(server.repl_backlog->blocks_index) * sizeof(void*);
}
mem_total += mh->repl_backlog;
mem_total += mh->clients_slaves;

具體而言,如果全局複製緩衝區的大小大於repl-backlog-size,則複製積壓緩衝區(mh->repl_backlog)的大小取 repl-backlog-size,剩餘部分視為從庫使用的記憶體(mh->clients_slaves)。如果全局複製緩衝區的大小小於等於 repl-backlog-size,則直接取全局複製緩衝區的大小。

此外,由於引入了一個 Rax 樹來索引全局複製緩衝區中的部分節點,複製積壓緩衝區還需要計算 Rax 樹的記憶體開銷。

數據驅逐的觸發條件

很多人有個誤區,認為只要 used_memory 大於 maxmemory ,就會觸發數據的驅逐。但實際上不是。

數據被驅逐需滿足以下條件:

  1. maxmemory 必須大於 0。
  2. maxmemory-policy 不能是 noeviction。
  3. 記憶體使用需滿足一定的條件。不是 used_memory 大於 maxmemory,而是 used_memory 減去 mem_not_counted_for_evict 後的值大於 maxmemory。

其中,mem_not_counted_for_evict的值可以通過 INFO 命令獲取,它的大小是在freeMemoryGetNotCountedMemory函數中計算的。

size_t freeMemoryGetNotCountedMemory(void) {
    size_t overhead = 0;
    int slaves = listLength(server.slaves);

    if (slaves) {
        listIter li;
        listNode *ln;

        listRewind(server.slaves,&li);
        while((ln = listNext(&li))) {
            client *slave = listNodeValue(ln);
            overhead += getClientOutputBufferMemoryUsage(slave);
        }
    }
    if (server.aof_state != AOF_OFF) {
        overhead += sdsalloc(server.aof_buf)+aofRewriteBufferSize();
    }
    return overhead;
}

freeMemoryGetNotCountedMemory函數統計了所有從節點的複製緩存區、AOF 緩存區和 AOF 重寫緩衝區的總大小。

所以,在 Redis 判斷是否需要驅逐數據時,會從used_memory中剔除從節點複製緩存區、AOF 緩存區以及 AOF 重寫緩衝區的記憶體占用。

Redis 記憶體分析腳本

最後,分享一個腳本。

這個腳本能夠幫助我們快速分析 Redis 的記憶體使用情況。通過輸出結果,我們可以直觀地查看 Redis 各個部分的記憶體消耗情況並識別當 used_memory 增加時,具體是哪一部分的記憶體消耗導致的。

腳本地址:https://github.com/slowtech/dba-toolkit/blob/master/redis/redis_mem_usage_analyzer.py

# python3 redis_mem_usage_analyzer.py -host 10.0.1.182 -p 6379
Metric(2024-09-12 04:52:42)    Old Value            New Value(+3s)       Change per second   
==========================================================================================
Summary
---------------------------------------------
used_memory                    16.43G               16.44G               1.1M                
used_memory_dataset            11.93G               11.93G               22.66K              
used_memory_overhead           4.51G                4.51G                1.08M               

Overhead(Total)                4.51G                4.51G                1.08M               
---------------------------------------------
mem_clients_normal             440.57K              440.52K              -18.67B             
mem_clients_slaves             458.41M              461.63M              1.08M               
mem_replication_backlog        160M                 160M                 0B                  
mem_aof_buffer                 0B                   0B                   0B                  
used_memory_startup            793.17K              793.17K              0B                  
used_memory_scripts            0B                   0B                   0B                  
mem_hashtable                  3.9G                 3.9G                 0B                  

Evict & Fragmentation
---------------------------------------------
maxmemory                      20G                  20G                  0B                  
mem_not_counted_for_evict      458.45M              461.73M              1.1M                
mem_counted_for_evict          15.99G               15.99G               2.62K               
maxmemory_policy               volatile-lru         volatile-lru                             
used_memory_peak               16.43G               16.44G               1.1M                
used_memory_rss                16.77G               16.77G               1.32M               
mem_fragmentation_bytes        345.07M              345.75M              232.88K             

Others
---------------------------------------------
keys                           77860000             77860000             0.0                 
instantaneous_ops_per_sec      8339                 8435                                     
lazyfree_pending_objects       0                    0                    0.0       

該腳本每隔一段時間(由 -i 參數決定,預設是 3 秒)採集一次 Redis 的記憶體數據。然後,它會將當前採集到的數據(New Value)與上一次的數據(Old Value)進行對比,計算出每秒的增量(Change per second)。

輸出主要分為四大部分:

  • Summary:彙總部分,used_memory = used_memory_dataset + used_memory_overhead。
  • Overhead(Total):展示 used_memory_overhead 中各個細項的記憶體消耗情況。Overhead(Total) 等於所有細項之和,理論上應與 used_memory_overhead 相等。
  • Evict & Fragmentation:顯示驅逐和記憶體碎片的一些關鍵指標。其中,mem_counted_for_evict = used_memory - mem_not_counted_for_evict,當 mem_counted_for_evict 超過 maxmemory 時,才會觸發數據驅逐。
  • Others:其他一些重要指標,包括 keys(鍵的總數量)、instantaneous_ops_per_sec(每秒操作數)以及 lazyfree_pending_objects(通過非同步刪除等待釋放的對象數)。

如果發現mem_clients_normalmem_clients_slaves比較大,可指定 --client 查看客戶端的記憶體使用情況。

# python3 redis_mem_usage_analyzer.py -host 10.0.1.182 -p 6379 --client
ID    Address            Name  Age    Command         User     Qbuf       Omem       Total Memory   
----------------------------------------------------------------------------------------------------
216   10.0.1.75:37811          721    psync           default  0B         232.83M    232.85M        
217   10.0.1.22:35057          715    psync           default  0B         232.11M    232.13M        
453   10.0.0.198:51172         0      client          default  26B        0B         60.03K         
...  

其中,

  • Qbuf:輸入緩衝區的大小。
  • Omem:輸出緩衝區的大小。
  • Total Memory:該連接占用的總記憶體。

結果按 Total Memory 從大到小的順序輸出。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ladp服務搭建 用戶名:cn=admin,dc=test,dc=com 密碼:123456 1)軟體安裝yum install openldap openldap-clients openldap-servers -y2)配置 OpenLDAP Servervim /etc/openldap/sl ...
  • liwen01 2024.09.16 前言 802.11 無線 WiFi 網有三類幀:數據幀、管理幀、控制幀。與有線網相比,無線 WiFi 網會複雜很多。大部分應用軟體開發對 WiFi 的控制幀和管理幀瞭解得並不多,因為它們在物理層和數據鏈路層就已經被處理了,上層應用很少能感知到。 一般是在設備出現 ...
  • 寫在前面 本隨筆是非常菜的菜雞寫的。如有問題請及時提出。 可以聯繫:[email protected] GitHhub:https://github.com/WindDevil (目前啥也沒有 引言 兜兜轉轉又是新的一章的開始,還是首先要看官方手冊里的理論介紹和內容. 這裡主要還是提綱挈領地摘抄裡面 ...
  • 金葫蘆STM32L431上手流程教材書名和開發板教材:《嵌入式技術基礎與實踐(第6版)》(王宜懷主編)開發板:AHL-STM32L431金葫蘆STM32L431上手流程1、需要用到的軟體和電子資源 AHL-GEC-IDE(4.55)————>AHL-GEC-IDE (suda.edu.cn)AHL- ...
  • 筆者出於學習(折騰)原因想要改換 Linux 發行版,於是將目光投向大名鼎鼎的 ArchLinux。 ArchLinux 的安裝過程漫長且複雜,遂彙總成小記,以備日後參考。 小記中筆者共使用兩塊硬碟,一塊已含有 Windows10 分區,另一塊作為雙系統數據共用盤。 LiveCD 本小記略過燒錄鏡像 ...
  • 出乎意料的現象 我們有一張測試表 t1,表中有一些數據,當 session1 開啟一個事務,並執行了 select for update 操作後仍未提交事務,在併發事務(如 session2)開啟事務並行執行一些操作會有不同的鎖現象,表現在: select for update 會出現鎖等待 del ...
  • Linux平臺安裝Oracle 19c的時候遇到了下麵錯誤“[INS-35180] Unable to check for available memory”,如圖所示: 具體的錯誤信息如下所示: Additional Information:Exception details - PRVG-190 ...
  • SQL Server的Descending Indexes降序索引 背景索引是關係型資料庫中優化查詢性能的重要手段之一。對於需要處理大量數據的場景,合理的索引策略能夠顯著減少查詢時間。 特別是在涉及多欄位排序的複雜查詢中,選擇合適的索引類型(如降序索引)顯得尤為重要。本文將探討如何在SQL Serv ...
一周排行
    -Advertisement-
    Play Games
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...