在高流量的網路應用中,有效管理Redis的記憶體使用至關重要。本文將介紹如何結合分片技術和位元操作,進一步提升Redis的記憶體使用效率,特別是在處理使用者位置資訊和唯一訪客計數等場景。我們將探討如何將使用者資料封裝到Redis字串中,並利用位元操作精確控制資料儲存,以減少記憶體開銷。同時,我們也會討論如何使用分片技術分散資料儲存,避免單一鍵值過大,並提升查詢效能。最後,文章將提供實用的Python程式碼範例,演示如何實作這些技術,並說明如何計算所有使用者和特定使用者群體的匯總資訊,以滿足不同資料分析需求。

最佳化記憶體使用:分片技術與位元操作

在前一章節中,我們討論瞭如何利用分片技術最佳化大規模資料集的儲存與查詢效率。本章節將探討如何進一步減少記憶體使用,特別是在處理大量序號ID的使用者資料時。

分片集合(Sharded Sets)技術最佳化

在處理大量唯一訪客計數的場景中,我們利用了分片集合技術來最佳化記憶體使用。以下是一個實際的Python範例,展示瞭如何計算每日的預期唯一訪客數量:

import redis
import math
from datetime import datetime, timedelta

# 初始化Redis連線
conn = redis.Redis(host='localhost', port=6379, db=0)

# 定義預期每日訪客數的預設值
DAILY_EXPECTED = 1000000  # 預設每日訪客數

# 定義快取字典
EXPECTED = {}

def calculate_expected_visitors(key):
    today = datetime.now().date()
    if key in EXPECTED:
        return EXPECTED[key]
    
    exkey = key + ':expected'
    expected = conn.get(exkey)
    
    if not expected:
        yesterday = (today - timedelta(days=1)).isoformat()
        expected = conn.get('unique:%s' % yesterday)
        expected = int(expected or DAILY_EXPECTED)
        expected = 2 ** int(math.ceil(math.log(expected * 1.5, 2)))
        
        if not conn.setnx(exkey, expected):
            expected = conn.get(exkey)
    
    EXPECTED[key] = int(expected)
    return EXPECTED[key]

#### 內容解密:
此函式的主要目的是計算當日的預期唯一訪客數量首先它會檢查是否已經計算過該數值如果有則直接傳回快取中的數值如果沒有它會嘗試從Redis中取得昨日的唯一訪客數並在其基礎上增加50%然後向上取整到最接近的2的冪次方如果其他程式已經計算並儲存了當日的預期訪客數則使用該數值

### 分片集合的儲存優勢

透過使用分片集合技術我們成功地將儲存空間需求從56 MB減少到9.5 MB節省了約83%的記憶體這意味著在相同的硬體條件下我們可以儲存約5.75倍的資料

### 擴充套件分片集合API

練習嘗試為分片集合實作`SREM``SISMEMBER`操作此外假設兩個分片集合具有相同的預期總數和分片大小可以嘗試實作`SINTERSTORE`、`SUNIONSTORE``SDIFFSTORE`的分片版本

### 封裝位元與位元組:Redis字串的高效利用

在某些場景下我們需要為具有序號ID的使用者儲存少量固定資訊Redis提供了`GETRANGE`、`SETRANGE`、`GETBIT``SETBIT`等命令讓我們能夠高效地操作字串中的位元和位元組

#### 儲存位置資訊的範例

假設我們需要儲存使用者的地區/州級別資訊可以使用2個位元組來實作這需要我們首先了解如何使用Redis字串來儲存和更新這類別資訊

```python
# 定義國家程式碼和地區資訊
COUNTRIES = '''
ABW AFG AGO AIA ALA ALB AND ARE ARG ARM ASM ATA ATF ATG AUS AUT AZE BDI
BEL BEN BES BFA BGD BGR BHR BHS BIH BLM BLR BLZ BMU BOL BRA BRB BRN BTN
BVT BWA CAF CAN CCK CHE CHL CHN CIV CMR COD COG COK COL COM CPV CRI CUB
CUW CXR CYM CYP CZE DEU DJI DMA DNK DOM DZA ECU EGY ERI ESH ESP EST ETH
FIN FJI FLK FRA FRO FSM GAB GBR GEO GGY GHA GIB GIN GLP GMB GNB GNQ GRC
GRD GRL GTM GUF GUM GUY HKG HMD HND HRV HTI HUN IDN IMN IND IOT IRL IRN
IRQ ISL ISR ITA JAM JEY JOR JPN KAZ KEN KGZ KHM KIR KNA KOR KWT LAO LBN
LBR LBY LCA LIE LKA LSO LTU LUX LVA MAC MAF MAR MCO MDA MDG MDV MEX MHL
MKD MLI MLT MMR MNE MNG MNP MOZ MRT MSR MTQ MUS MWI MYS MYT NAM NCL NER
NFK NGA NIC NIU NLD NOR NPL NRU NZL OMN PAK PAN PCN PER PHL PLW PNG POL
'''

# 將國家程式碼和地區資訊儲存到Redis中
def store_location_info(conn, country_code, region_code):
    # 使用SETRANGE命令儲存地區資訊
    conn.setrange('location:%s' % country_code, 0, region_code)

#### 內容解密:
此範例展示瞭如何使用Redis的`SETRANGE`命令來儲存特定國家程式碼對應的地區資訊透過這種方式我們可以高效地利用Redis字串儲存大量使用者的位置資料

### 地區資訊儲存的精確度選擇

根據不同的應用場景我們可以選擇不同的精確度來儲存地區資訊例如使用1個位元組可以儲存全球的國家級別資訊使用2個位元組可以儲存地區/州級別的資訊使用3個位元組可以儲存區域郵政編碼使用4個位元組可以儲存經緯度資訊精確度可達2米或6英尺

## 資料封裝與儲存最佳化
在處理大量使用者資料時如何有效率地儲存和檢索資料是一個重要的技術挑戰本章節將探討如何利用 Redis 進行資料封裝和儲存的最佳化特別是在處理使用者位置資訊的場景下

### 資料表的建立
首先我們需要定義一些基礎的資料表用於儲存國家地區和省份的程式碼這些資料表是以字串的形式定義並透過 `split()` 方法轉換成列表

```python
COUNTRIES = '''AFG ALB ALG AND ARE ARG ARM ASM ATA ATG AUS AUT AZE BDI
BEL BEN BFA BGD BGR BHR BHS BIH BLM BLR BLZ BMU BOL BRA BRB BRN BTN BWA
CAF CAN CCK CHE CHI CHL CHN CIV CMR COD COG COL COM CPV CRI CUB CXR CYM
CYP CZE DEU DJI DMA DNK DOM DZA ECU EGY ERI ESH ESP EST ETH FIN FJI FLK
FRA FRO FSM GAB GBR GBS GHA GIN GMB GNB GRC GRD GRL GTM GUM GUY HKG HND
HRV HTI HUN IDN IND IRL IRN IRQ ISL ISR ITA JAM JOR JPN KAZ KEN KGZ KHM
KIR KNA KOR KWT LAO LBN LBR LBY LCA LIE LKA LSO LTU LUX LVA MAC MAF MAR
MCO MEX MDA MDG MDV MEK MHL MKD MLI MLT MMR MNE MNG MNP MOZ MRT MSR MTQ
MUS MWI MYS MYT NAM NCL NER NFK NGA NIC NIU NLD NOR NPL NRU NZL OMA PAK
PAN PCN PER PHL PLW PNG POL PRI PRK PRT PRY PSE PYF QAT REU ROU RUS RWA
SAU SDN SEN SGP SGS SHN SJM SLB SLE SLV SMR SOM SPM SRB SSD STP SUR SVK
SVN SWE SWZ SXM SYC SYR TCA TCD TGO THA TJK TKL TKM TLS TON TTO TUN TUR
TUV TWN TZA UGA UKR UMI URY USA UZB VAT VCT VEN VGB VIR VNM VUT WLF WSM
YEM ZAF ZMB ZWE'''.split()

STATES = {
    'CAN': '''AB BC MB NB NL NS NT NU ON PE QC SK YT'''.split(),
    'USA': '''AA AE AK AL AP AR AS AZ CA CO CT DC DE FL FM GA GU HI IA ID
IL IN KS KY LA MA MD ME MH MI MN MO MP MS MT NC ND NE NH NJ NM NV NY OH
OK OR PA PR PW RI SC SD TN TX UT VA VI VT WA WI WV WY'''.split(),
}

國家與地區程式碼的計算

接下來,我們需要將國家和地區資訊轉換成一個 2 位元組的程式碼。這可以透過查詢國家和地區在對應列表中的索引來實作。

def get_code(country, state):
    cindex = bisect.bisect_left(COUNTRIES, country)
    if cindex > len(COUNTRIES) or COUNTRIES[cindex] != country:
        cindex = -1
    cindex += 1
    sindex = -1
    if state and country in STATES:
        states = STATES[country]
        sindex = bisect.bisect_left(states, state)
        if sindex > len(states) or states[sindex] != state:
            sindex = -1
        sindex += 1
    return chr(cindex) + chr(sindex)

內容解密:

  1. 國家索引計算:使用 bisect.bisect_left 函式在 COUNTRIES 列表中找到國家的索引。如果國家不存在,則將索引設為 -1,並加 1 以避免使用 0 作為未找到的標誌。
  2. 地區索引計算:如果提供了地區資訊且該國家有對應的地區列表,則在該列表中查詢地區的索引。同樣地,如果地區不存在,則將索引設為 -1,並加 1 以進行後續處理。
  3. 程式碼生成:將國家和地區的索引轉換成 ASCII 字元,並組合成一個 2 位元組的程式碼傳回。

資料儲存最佳化

在計算出國家和地區程式碼後,我們需要將這些資訊以高效的方式儲存在 Redis 中。由於 Redis 對單一字串的大小有限制(512 MB),我們需要將資料分片儲存在多個字串中。

USERS_PER_SHARD = 2**20

def set_location(conn, user_id, country, state):
    code = get_code(country, state)
    shard_id, position = divmod(user_id, USERS_PER_SHARD)
    offset = position * 2
    pipe = conn.pipeline(False)
    pipe.setrange('location:%s' % shard_id, offset, code)
    tkey = str(uuid.uuid4())
    pipe.zadd(tkey, {'max': user_id})
    pipe.zunionstore('location:max', [tkey, 'location:max'], aggregate='max')
    pipe.delete(tkey)
    pipe.execute()

內容解密:

  1. 分片 ID 和位置計算:根據使用者 ID 計算出分片 ID 和在該分片中的位置。
  2. 偏移量計算:根據使用者在分片中的位置計算出資料的偏移量。
  3. 資料儲存:使用 SETTERANGE 命令將國家和地區程式碼儲存在對應的分片中。
  4. 最大使用者 ID 更新:使用 ZADDZUNIONSTORE 命令更新儲存最大使用者 ID 的有序集合。

減少Redis記憶體使用的方法

在現代的資料處理和分析中,Redis因其高效的效能而被廣泛使用。然而,隨著資料量的增加,如何有效管理Redis的記憶體使用成為了一個重要的課題。本章節將探討幾種減少Redis記憶體使用的方法,包括使用簡短的資料結構、將大型資料結構進行分片處理,以及將資料直接封裝到STRING型別中。

9.3 使用短資料結構和分片技術

在Redis中,資料結構的選擇和使用方式會直接影響記憶體的使用效率。對於一些大型資料結構,如儲存大量使用者的位置資訊,使用合適的資料結構和分片技術可以顯著減少記憶體的使用。

9.3.1 將資料封裝到STRING中

為了減少記憶體的使用,我們可以將多個資料封裝到一個STRING型別的鍵值對中。這種方法尤其適用於那些具有固定長度屬性的資料。例如,在儲存使用者的位置資訊時,可以將國家和州的程式碼轉換為兩個位元組的程式碼,並將這些程式碼順序儲存到一個STRING中。

def update_aggregates(countries, states, codes):
    for code in codes:
        if len(code) != 2:
            continue
        country = ord(code[0]) - 1
        state = ord(code[1]) - 1
        if country < 0 or country >= len(COUNTRIES):
            continue
        country = COUNTRIES[country]
        countries[country] += 1
        if country not in STATES:
            continue
        if state < 0 or state >= len(STATES[country]):
            continue
        state = STATES[country][state]
        states[country][state] += 1

#### 內容解密:

此函式的作用是將國家和州的程式碼轉換為實際的國家和州名稱,並更新相應的計數器。首先,檢查程式碼的長度是否正確,然後將程式碼轉換為國家和州的索引。如果國家或州的索引超出有效範圍,則跳過該程式碼。否則,將國家和州的計數器加一。

9.3.2 使用ZSET儲存最高編號的使用者ID

除了將資料封裝到STRING中,我們還使用了一個ZSET來儲存已知的最高編號的使用者ID。這對於計算所有使用者的匯總資訊非常重要,因為它幫助我們知道何時停止遍歷使用者ID。

9.3.3 計算分片STRING的匯總資訊

計算匯總資訊是資料分析中的一個重要步驟。我們可以計算所有使用者的匯總資訊,也可以計算特定使用者群體的匯總資訊。

計算所有使用者的匯總資訊

為了計算所有使用者的匯總資訊,我們重用了之前寫的readblocks()函式,該函式可以從給定的鍵中讀取資料區塊。使用這個函式,我們可以一次性提取數千個使用者的資訊。

def aggregate_location(conn):
    countries = defaultdict(int)
    states = defaultdict(lambda: defaultdict(int))
    max_id = int(conn.zscore('location:max', 'max'))
    max_block = max_id // USERS_PER_SHARD
    for shard_id in xrange(max_block + 1):
        for block in readblocks(conn, 'location:%s' % shard_id):
            for offset in xrange(0, len(block) - 1, 2):
                code = block[offset:offset + 2]
                update_aggregates(countries, states, [code])
    return countries, states

#### 內容解密:

此函式用於計算所有使用者的位置匯總資訊。首先,從ZSET中取得已知的最高使用者ID,並計算需要遍歷的分片數量。然後,遍歷每個分片,並使用readblocks()函式讀取每個分片中的資料區塊。對於每個資料區塊,提取國家和州的程式碼,並使用update_aggregates()函式更新匯總資訊。

計算特定使用者群體的匯總資訊

除了計算所有使用者的匯總資訊,我們還可以計算特定使用者群體的匯總資訊。例如,如果我們有某個使用者的關注者列表,我們可以提取這些關注者的位置資訊,並計算他們的匯總資訊。

def aggregate_location_list(conn, user_ids):
    pipe = conn.pipeline(False)
    countries = defaultdict(int)
    states = defaultdict(lambda: defaultdict(int))
    for i, user_id in enumerate(user_ids):
        shard_id, position = divmod(user_id, USERS_PER_SHARD)
        offset = position * 2
        pipe.substr('location:%s' % shard_id, offset, offset + 1)
        if (i + 1) % 1000 == 0:
            update_aggregates(countries, states, pipe.execute())
    update_aggregates(countries, states, pipe.execute())
    return countries, states

#### 內容解密:

此函式用於計算特定使用者列表的位置匯總資訊。首先,設定一個pipeline來減少對Redis的請求次數。然後,遍歷使用者ID列表,計算每個使用者的位置資訊在分片中的偏移量,並使用pipeline提取這些資訊。每1000個使用者提取一次資訊,並更新匯總資訊。最後,傳回匯總資訊。