Redis 到底是如何實現“附近的人”這個功能呢?

来源:https://www.cnblogs.com/winner192/archive/2019/11/02/11782308.html
-Advertisement-
Play Games

針對“附近的人”這一位置服務領域的應用場景,常見的可使用PG、MySQL和MongoDB等多種DB的空間索引進行實現。而Redis另闢蹊徑,結合其有序隊列zset以及geohash編碼,實現了空間搜索功能,且擁有極高的運行效率。本文將從源碼角度對其演算法原理進行解析,並推算查詢時間複雜度。 操作命令 ...


針對“附近的人”這一位置服務領域的應用場景,常見的可使用PG、MySQL和MongoDB等多種DB的空間索引進行實現。而Redis另闢蹊徑,結合其有序隊列zset以及geohash編碼,實現了空間搜索功能,且擁有極高的運行效率。本文將從源碼角度對其演算法原理進行解析,並推算查詢時間複雜度。

操作命令

自Redis 3.2開始,Redis基於geohash和有序集合提供了地理位置相關功能。

Redis Geo模塊包含了以下6個命令:

GEOADD: 將給定的位置對象(緯度、經度、名字)添加到指定的key;

GEOPOS: 從key裡面返回所有給定位置對象的位置(經度和緯度);

GEODIST: 返回兩個給定位置之間的距離;

GEOHASH: 返回一個或多個位置對象的Geohash表示;

GEORADIUS: 以給定的經緯度為中心,返回目標集合中與中心的距離不超過給定最大距離的所有位置對象;

GEORADIUSBYMEMBER: 以給定的位置對象為中心,返回與其距離不超過給定最大距離的所有位置對象。

 

其中,組合使用GEOADD和GEORADIUS可實現“附近的人”中“增”和“查”的基本功能。要實現微信中“附近的人”功能,可直接使用GEORADIUSBYMEMBER命令。其中“給定的位置對象”即為用戶本人,搜索的對象為其他用戶。不過本質上,GEORADIUSBYMEMBER = GEOPOS + GEORADIUS,即先查找用戶位置再通過該位置搜索附近滿足位置相互距離條件的其他用戶對象。

以下會從源碼角度入手對GEOADD和GEORADIUS命令進行分析,剖析其演算法原理。

 

Redis geo操作中只包含了“增”和“查”的操作,並沒有專門的“刪除”命令。主要是因為Redis內部使用有序集合(zset)保存位置對象,可用zrem進行刪除。

 

在Redis源碼geo.c的文件註釋中,只說明瞭該文件為GEOADD、GEORADIUS和GEORADIUSBYMEMBER的實現文件(其實在也實現了另三個命令)。從側面看出其他三個命令為輔助命令。

 

GEOADD

使用方式

GEOADD key longitude latitude member [longitude latitude member ...]

複製代碼將給定的位置對象(緯度、經度、名字)添加到指定的key。

其中,key為集合名稱,member為該經緯度所對應的對象。在實際運用中,當所需存儲的對象數量過多時,可通過設置多key(如一個省一個key)的方式對對象集合變相做sharding,避免單集合數量過多。

成功插入後的返回值:

(integer) N

複製代碼其中N為成功插入的個數。

源碼分析

/* GEOADD key long lat name [long2 lat2 name2 ... longN latN nameN] */

void geoaddCommand(client *c) {

 

//參數校驗

    /* Check arguments number for sanity. */

    if ((c->argc - 2) % 3 != 0) {

        /* Need an odd number of arguments if we got this far... */

        addReplyError(c, "syntax error. Try GEOADD key [x1] [y1] [name1] "

                         "[x2] [y2] [name2] ... ");

        return;

    }

 

//參數提取Redis

    int elements = (c->argc - 2) / 3;

    int argc = 2+elements*2; /* ZADD key score ele ... */

    robj **argv = zcalloc(argc*sizeof(robj*));

    argv[0] = createRawStringObject("zadd",4);

    argv[1] = c->argv[1]; /* key */

    incrRefCount(argv[1]);

 

//參數遍歷+轉換

    /* Create the argument vector to call ZADD in order to add all

     * the score,value pairs to the requested zset, where score is actually

     * an encoded version of lat,long. */

    int i;

    for (i = 0; i < elements; i++) {

        double xy[2];

 

    //提取經緯度

        if (extractLongLatOrReply(c, (c->argv+2)+(i*3),xy) == C_ERR) {

            for (i = 0; i < argc; i++)

                if (argv[i]) decrRefCount(argv[i]);

            zfree(argv);

            return;

        }

    

    //將經緯度轉換為52位的geohash作為分值 & 提取對象名稱

        /* Turn the coordinates into the score of the element. */

        GeoHashBits hash;

        geohashEncodeWGS84(xy[0], xy[1], GEO_STEP_MAX, &hash);

        GeoHashFix52Bits bits = geohashAlign52Bits(hash);

        robj *score = createObject(OBJ_STRING, sdsfromlonglong(bits));

        robj *val = c->argv[2 + i * 3 + 2];

 

    //設置有序集合的對象元素名稱和分值

        argv[2+i*2] = score;

        argv[3+i*2] = val;

        incrRefCount(val);

    }

 

//調用zadd命令,存儲轉化好的對象

    /* Finally call ZADD that will do the work for us. */

    replaceClientCommandVector(c,argc,argv);

    zaddCommand(c);

}

複製代碼通過源碼分析可以看出Redis內部使用有序集合(zset)保存位置對象,有序集合中每個元素都是一個帶位置的對象,元素的score值為其經緯度對應的52位的geohash值。

 

double類型精度為52位;

geohash是以base32的方式編碼,52bits最高可存儲10位geohash值,對應地理區域大小為0.6*0.6米的格子。換句話說經Redis geo轉換過的位置理論上會有約0.3*1.414=0.424米的誤差。

 

演算法小結

總結下GEOADD命令都幹了啥:

1、參數提取和校驗;

2、將入參經緯度轉換為52位的geohash值(score);

3、調用ZADD命令將member及其對應的score存入集合key中。
 

GEORADIUS

使用方式

GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [ASC|DESC] [COUNT count] [STORE key] [STORedisT key]

複製代碼以給定的經緯度為中心,返回目標集合中與中心的距離不超過給定最大距離的所有位置對象。

範圍單位:m | km | ft | mi --> 米 | 千米 | 英尺 | 英里

額外參數:

- WITHDIST:在返回位置對象的同時,將位置對象與中心之間的距離也一併返回。距離的單位和用戶給定的範圍單位保持一致。

- WITHCOORD:將位置對象的經度和維度也一併返回。

- WITHHASH:以 52 位有符號整數的形式,返回位置對象經過原始 geohash 編碼的有序集合分值。這個選項主要用於底層應用或者調試,實際中的作用並不大。

- ASC|DESC:從近到遠返回位置對象元素 | 從遠到近返回位置對象元素。

- COUNT count:選取前N個匹配位置對象元素。(不設置則返回所有元素)

- STORE key:將返回結果的地理位置信息保存到指定key。

- STORedisT key:將返回結果離中心點的距離保存到指定key。

 

由於 STORE 和 STORedisT 兩個選項的存在,GEORADIUS 和 GEORADIUSBYMEMBER 命令在技術上會被標記為寫入命令,從而只會查詢(寫入)主實例,QPS過高時容易造成主實例讀寫壓力過大。

為解決這個問題,在 Redis 3.2.10 和 Redis 4.0.0 中,分別新增了 GEORADIUS_RO 和 GEORADIUSBYMEMBER_RO兩個只讀命令。

 

成功查詢後的返回值:

不帶WITH限定,返回一個member list,如:

["member1","member2","member3"]

複製代碼帶WITH限定,member list中每個member也是一個嵌套list,如:

[

["member1", distance1, [longitude1, latitude1]]

["member2", distance2, [longitude2, latitude2]]

]

 

複製代碼源碼分析

 

此段源碼較長,看不下去的可直接看中文註釋,或直接跳到小結部分

 

/* GEORADIUS key x y radius unit [WITHDIST] [WITHHASH] [WITHCOORD] [ASC|DESC]

 *                               [COUNT count] [STORE key] [STORedisT key]

 * GEORADIUSBYMEMBER key member radius unit ... options ... */

void georadiusGeneric(client *c, int flags) {

    robj *key = c->argv[1];

    robj *storekey = NULL;

    int stoRedist = 0; /* 0 for STORE, 1 for STORedisT. */

 

//根據key獲取有序集合

    robj *zobj = NULL;

    if ((zobj = lookupKeyReadOrReply(c, key, shared.null[c->resp])) == NULL ||

        checkType(c, zobj, OBJ_ZSET)) {

        return;

    }

 

//根據用戶輸入(經緯度/member)確認中心點經緯度

    int base_args;

    double xy[2] = { 0 };

    if (flags & RADIUS_COORDS) {

……

    }

 

//獲取查詢範圍距離

    double radius_meters = 0, conversion = 1;

    if ((radius_meters = extractDistanceOrReply(c, c->argv + base_args - 2,

                                                &conversion)) < 0) {

        return;

    }

 

//獲取可選參數 (withdist、withhash、withcoords、sort、count)

    int withdist = 0, withhash = 0, withcoords = 0;

    int sort = SORT_NONE;

    long long count = 0;

    if (c->argc > base_args) {

        ... ...

    }

 

//獲取 STORE 和 STORedisT 參數

    if (storekey && (withdist || withhash || withcoords)) {

        addReplyError(c,

            "STORE option in GEORADIUS is not compatible with "

            "WITHDIST, WITHHASH and WITHCOORDS options");

        return;

    }

 

//設定排序

    if (count != 0 && sort == SORT_NONE) sort = SORT_ASC;

 

//利用中心點和半徑計算目標區域範圍

    GeoHashRadius georadius =

        geohashGetAreasByRadiusWGS84(xy[0], xy[1], radius_meters);

 

//對中心點及其周圍8個geohash網格區域進行查找,找出範圍內元素對象

    geoArray *ga = geoArrayCreate();

    membersOfAllNeighbors(zobj, georadius, xy[0], xy[1], radius_meters, ga);

 

//未匹配返空

    /* If no matching results, the user gets an empty reply. */

    if (ga->used == 0 && storekey == NULL) {

        addReplyNull(c);

        geoArrayFree(ga);

        return;

    }

 

//一些返回值的設定和返回

    ……

    geoArrayFree(ga);

}

 

複製代碼上文代碼中最核心的步驟有兩個,一是“計算中心點範圍”,二是“對中心點及其周圍8個geohash網格區域進行查找”。對應的是geohashGetAreasByRadiusWGS84和membersOfAllNeighbors兩個函數。我們依次來看:

 

計算中心點範圍:

 

// geohash_helper.c

GeoHashRadius geohashGetAreasByRadiusWGS84(double longitude, double latitude,

                                           double radius_meters) {

    return geohashGetAreasByRadius(longitude, latitude, radius_meters);

}

 

//返回能夠覆蓋目標區域範圍的9個geohashBox

GeoHashRadius geohashGetAreasByRadius(double longitude, double latitude, double radius_meters) {

//一些參數設置

    GeoHashRange long_range, lat_range;

    GeoHashRadius radius;

    GeoHashBits hash;

    GeoHashNeighbors neighbors;

    GeoHashArea area;

    double min_lon, max_lon, min_lat, max_lat;

    double bounds[4];

    int steps;

 

//計算目標區域外接矩形的經緯度範圍(目標區域為:以目標經緯度為中心,半徑為指定距離的圓)

    geohashBoundingBox(longitude, latitude, radius_meters, bounds);

    min_lon = bounds[0];

    min_lat = bounds[1];

    max_lon = bounds[2];

    max_lat = bounds[3];

 

//根據目標區域中心點緯度和半徑,計算帶查詢的9個搜索框的geohash精度(位)

//這裡用到latitude主要是針對極地的情況對精度進行了一些調整(緯度越高,位數越小)

    steps = geohashEstimateStepsByRadius(radius_meters,latitude);

 

//設置經緯度最大最小值:-180<=longitude<=180, -85<=latitude<=85

    geohashGetCoordRange(&long_range,&lat_range);

    

//將待查經緯度按指定精度(steps)編碼成geohash值

    geohashEncode(&long_range,&lat_range,longitude,latitude,steps,&hash);

    

//將geohash值在8個方向上進行擴充,確定周圍8個Box(neighbors)

    geohashNeighbors(&hash,&neighbors);

    

//根據hash值確定area經緯度範圍

    geohashDecode(long_range,lat_range,hash,&area);

 

//一些特殊情況處理

    ……

 

//構建並返回結果    

    radius.hash = hash;

    radius.neighbors = neighbors;

    radius.area = area;

    return radius;

}

 

複製代碼

對中心點及其周圍8個geohash網格區域進行查找:

 

// geo.c

//在9個hashBox中獲取想要的元素

int membersOfAllNeighbors(robj *zobj, GeoHashRadius n, double lon, double lat, double radius, geoArray *ga) {

    GeoHashBits neighbors[9];

    unsigned int i, count = 0, last_processed = 0;

    int debugmsg = 0;

 

//獲取9個搜索hashBox

    neighbors[0] = n.hash;

    neighbors[8] = n.neighbors.south_west;

 

//在每個hashBox中搜索目標點

    for (i = 0; i < sizeof(neighbors) / sizeof(*neighbors); i++) {

        if (HASHISZERO(neighbors[i])) {

            if (debugmsg) D("neighbors[%d] is zero",i);

            continue;

        }

 

//剔除可能的重覆hashBox (搜索半徑>5000KM時可能出現)

        if (last_processed &&

            neighbors[i].bits == neighbors[last_processed].bits &&

            neighbors[i].step == neighbors[last_processed].step)

        {

            continue;

        }

 

//搜索hashBox中滿足條件的對象    

        count += membersOfGeoHashBox(zobj, neighbors[i], ga, lon, lat, radius);

        last_processed = i;

    }

    return count;

}

 

 

int membersOfGeoHashBox(robj *zobj, GeoHashBits hash, geoArray *ga, double lon, double lat, double radius) {

//獲取hashBox內的最大、最小geohash值(52位)

    GeoHashFix52Bits min, max;

    scoresOfGeoHashBox(hash,&min,&max);

 

//根據最大、最小geohash值篩選zobj集合中滿足條件的點

    return geoGetPointsInRange(zobj, min, max, lon, lat, radius, ga);

}

 

 

int geoGetPointsInRange(robj *zobj, double min, double max, double lon, double lat, double radius, geoArray *ga) {

 

//搜索Range的參數邊界設置(即9個hashBox其中一個的邊界範圍)

    zrangespec range = { .min = min, .max = max, .minex = 0, .maxex = 1 };

    size_t origincount = ga->used;

    sds member;

 

//搜索集合zobj可能有ZIPLIST和SKIPLIST兩種編碼方式,這裡以SKIPLIST為例,邏輯是一樣的

    if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {

        ……

    } else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {

        zset *zs = zobj->ptr;

        zskiplist *zsl = zs->zsl;

        zskiplistNode *ln;

 

//獲取在hashBox範圍內的首個元素(跳錶數據結構,效率可比擬於二叉查找樹),沒有則返0

        if ((ln = zslFirstInRange(zsl, &range)) == NULL) {

            /* Nothing exists starting at our min.  No results. */

            return 0;

        }

 

//從首個元素開始遍歷集合

        while (ln) {

            sds ele = ln->ele;

//遍歷元素超出range範圍則break

            /* Abort when the node is no longer in range. */

            if (!zslValueLteMax(ln->score, &range))

                break;

//元素校驗(計算元素與中心點的距離)

            ele = sdsdup(ele);

            if (geoAppendIfWithinRadius(ga,lon,lat,radius,ln->score,ele)

                == C_ERR) sdsfree(ele);

            ln = ln->level[0].forward;

        }

    }

    return ga->used - origincount;

}

 

int geoAppendIfWithinRadius(geoArray *ga, double lon, double lat, double radius, double score, sds member) {

    double distance, xy[2];

 

//解碼錯誤, 返回error

    if (!decodeGeohash(score,xy)) return C_ERR; /* Can't decode. */

 

//最終距離校驗(計算球面距離distance看是否小於radius)

    if (!geohashGetDistanceIfInRadiusWGS84(lon,lat, xy[0], xy[1],

                                           radius, &distance))

    {

        return C_ERR;

    }

 

//構建並返回滿足條件的元素

    geoPoint *gp = geoArrayAppend(ga);

    gp->longitude = xy[0];

    gp->latitude = xy[1];

    gp->dist = distance;

    gp->member = member;

    gp->score = score;

    return C_OK;

}

 

 

複製代碼演算法小結

拋開眾多可選參數不談,簡單總結下GEORADIUS命令是怎麼利用geohash獲取目標位置對象的:

1、參數提取和校驗;

2、利用中心點和輸入半徑計算待查區域範圍。這個範圍參數包括滿足條件的最高的geohash網格等級(精度) 以及 對應的能夠覆蓋目標區域的九宮格位置;(後續會有詳細說明)

3、對九宮格進行遍歷,根據每個geohash網格的範圍框選出位置對象。進一步找出與中心點距離小於輸入半徑的對象,進行返回。

通過如下兩張圖在對演算法進行簡單的演示:

圖片

 

令左圖的中心為搜索中心,綠色圓形區域為目標區域,所有點為待搜索的位置對象,紅色點則為滿足條件的位置對象。

在實際搜索時,首先會根據搜索半徑計算geohash網格等級(即右圖中網格大小等級),並確定九宮格位置(即紅色九宮格位置信息);再依次查找計算九宮格中的點(藍點和紅點)與中心點的距離,最終篩選出距離範圍內的點(紅點)。

演算法分析

為什麼要用這種演算法策略進行查詢,或者說這種策略的優勢在哪,
 

為什麼要找到滿足條件的最高的geohash網格等級?為什麼用九宮格?
 

這其實是一個問題,本質上是對所有的元素對象進行了一次初步篩選。  在多層geohash網格中,每個低等級的geohash網格都是由4個高一級的網格拼接而成(如圖)。

圖片

換句話說,geohash網格等級越高,所覆蓋的地理位置範圍就越小。 當我們根據輸入半徑和中心點位置計算出的能夠覆蓋目標區域的最高等級的九宮格(網格)時,就已經對九宮格外的元素進行了篩除。 這裡之所以使用九宮格,而不用單個網格,主要原因還是為了避免邊界情況,儘可能縮小查詢區域範圍。試想以0經緯度為中心,就算查1米範圍,單個網格覆蓋的話也得查整個地球區域。而向四周八個方向擴展一圈可有效避免這個問題。

 

如何通過geohash網格的範圍框選出元素對象?效率如何?

首先在每個geohash網格中的geohash值都是連續的,有固定範圍。所以只要找出有序集合中,處在該範圍的位置對象即可。以下是有序集合的跳錶數據結構:
圖片
 

其擁有類似二叉查找樹的查詢效率,操作平均時間複雜性為O(log(N))。且最底層的所有元素都以鏈表的形式按序排列。所以在查詢時,只要找到集合中處在目標geohash網格中的第一個值,後續依次對比即可,不用多次查找。  九宮格不能一起查,要一個個遍歷的原因也在於九宮格各網格對應的geohash值不具有連續性。只有連續了,查詢效率才會高,不然要多做許多距離運算。

綜上,從源碼角度解析了Redis Geo模塊中 “增(GEOADD)” 和 “查(GEORADIUS)” 的詳細過程。並可推算出Redis中GEORADIUS查找附近的人功能,時間複雜度為:O(N+log(M)),其中N為指定半徑範圍內的位置元素數量,而M則是被九宮格圈住計算距離的元素的數量。結合Redis本身基於記憶體的存儲特性,在實際使用過程中有非常高的運行效率。

Redis的使用並不只有緩存的一個類型,而是針對不同的場景需要找到相關應用的數據類型來完善。對於Redis源碼有敢興趣的同學,可以加入群聊獲取,並交流相關問題


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

-Advertisement-
Play Games
更多相關文章
  • Visual Studio Code是一款很好的開源跨平臺代碼編輯器,這裡使用 tarball 格式文件來安裝(免安裝), 首先下載 .tar.gz 文件包,點擊下載, 可自行在官網下載 將文件包解壓,然後移動解壓後的文件夾到 /opt 目錄下, 輸入: sudo mv VSCode-linux-x ...
  • Windows鍵+ctrl+D 創建虛擬桌面 windows鍵+Ctrl+方向鍵 可以切換桌面 windows+tab ctrl+alt+TAB 切換應用 windows+[1-9] 打開任務欄固定的程式(先行放置) ctrl+alt+方向鍵 切歌 windows+方向鍵 分屏 CTRL+ALT+T ...
  • 本文系轉載,著作權歸作者所有。 商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。 作者: 宋寶華 來源: 微信公眾號linux閱碼場(id: linuxdev) 最初的2小時,你會愛上Docker,對原理和使用流程有個最基本的理解,避免滿世界無頭蒼蠅式找資料。本人反對暴風驟雨式多管齊下狂轟濫炸的學 ...
  • #查看字元集select userenv('language') from dual; select * from V$NLS_PARAMETERS; SELECT * FROM NLS_DATABASE_PARAMETERS where parameter='NLS_CHARACTERSET'; ... ...
  • 事務的基本特征 ACID Atomic(原子性) :事務中所有的操作是一個整體單元,這個單元中的操作要麼全部成功,要麼全部失敗,不會出現部分失敗、部分成功的場景; Consistency(一致性) :事務在完成時,必須使所有的數據都保持一致的狀態(約束 a + b = 10,事務結束後 a + b ...
  • 前述:這篇文檔是建立在三台虛擬機相互ping通,防火牆關閉,hosts文件修改,SSH 免密碼登錄,主機名修改等的基礎上開始的。 一.傳入文件 1.創建安裝目錄 mkdir /usr/local/soft 2.打開xftp,找到對應目錄,將所需安裝包傳入進去 查看安裝包:cd /usr/local/ ...
  • 前言 Redis 並沒有直接使用數據結構來實現鍵值對資料庫, 而是基於這些數據結構創建了一個對象系統, 這個系統包含字元串對象、列表對象、哈希對象、集合對象和有序集合對象這五種類型的對象, 每種對象都用到了至少一種我們前面所介紹的數據結構。 通過這五種不同類型的對象, Redis 可以在執行命令之前 ...
  • 在每次使用redis都進行連接的話會拉低redis的效率,都知道redis是基於記憶體的資料庫,效率賊高,所以每次進行連接比真正使用消耗的資源和時間還多。所以為了節省資源,減少多次連接損耗,連接池的作用相當於緩存了多個客戶端與redis服務端的連接,當有新的客戶端來進行連接時,此時,只需要去連接池獲取 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...