成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

穩了!用Redis實現“附近的人”功能

存儲 存儲軟件 Redis
針對“附近的人”這一位置服務領域的應用場景,常見的可使用 PG、MySQL 和 MongoDB 等多種 DB 的空間索引進行實現。

 針對“附近的人”這一位置服務領域的應用場景,常見的可使用 PG、MySQL 和 MongoDB 等多種 DB 的空間索引進行實現。

[[279964]]
圖片來自 Pexels 

而 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

使用方式

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

將給定的位置對象(緯度、經度、名字)添加到指定的 Key。其中,Key 為集合名稱,Member 為該經緯度所對應的對象。

在實際運用中,當所需存儲的對象數量過多時,可通過設置多 Key(如一個省一個 Key)的方式對對象集合變相做 Sharding,避免單集合數量過多。

成功插入后的返回值:

  1. (integer) N 

其中 N 為成功插入的個數。

源碼分析

  1. /* GEOADD key long lat name [long2 lat2 name2 ... longN latN nameN] */ 
  2. void geoaddCommand(client *c) { 
  3.  
  4. //參數校驗 
  5.     /* Check arguments number for sanity. */ 
  6.     if ((c->argc - 2) % 3 != 0) { 
  7.         /* Need an odd number of arguments if we got this far... */ 
  8.         addReplyError(c, "syntax error. Try GEOADD key [x1] [y1] [name1] " 
  9.                          "[x2] [y2] [name2] ... "); 
  10.         return
  11.     } 
  12.  
  13. //參數提取Redis 
  14.     int elements = (c->argc - 2) / 3; 
  15.     int argc = 2+elements*2; /* ZADD key score ele ... */ 
  16.     robj **argv = zcalloc(argc*sizeof(robj*)); 
  17.     argv[0] = createRawStringObject("zadd",4); 
  18.     argv[1] = c->argv[1]; /* key */ 
  19.     incrRefCount(argv[1]); 
  20.  
  21. //參數遍歷+轉換 
  22.     /* Create the argument vector to call ZADD in order to add all 
  23.      * the score,value pairs to the requested zset, where score is actually 
  24.      * an encoded version of lat,long. */ 
  25.     int i; 
  26.     for (i = 0; i < elements; i++) { 
  27.         double xy[2]; 
  28.  
  29.     //提取經緯度 
  30.         if (extractLongLatOrReply(c, (c->argv+2)+(i*3),xy) == C_ERR) { 
  31.             for (i = 0; i < argc; i++) 
  32.                 if (argv[i]) decrRefCount(argv[i]); 
  33.             zfree(argv); 
  34.             return
  35.         } 
  36.  
  37.     //將經緯度轉換為52位的geohash作為分值 & 提取對象名稱 
  38.         /* Turn the coordinates into the score of the element. */ 
  39.         GeoHashBits hash; 
  40.         geohashEncodeWGS84(xy[0], xy[1], GEO_STEP_MAX, &hash); 
  41.         GeoHashFix52Bits bits = geohashAlign52Bits(hash); 
  42.         robj *score = createObject(OBJ_STRING, sdsfromlonglong(bits)); 
  43.         robj *val = c->argv[2 + i * 3 + 2]; 
  44.  
  45.     //設置有序集合的對象元素名稱和分值 
  46.         argv[2+i*2] = score; 
  47.         argv[3+i*2] = val; 
  48.         incrRefCount(val); 
  49.     } 
  50.  
  51. //調用zadd命令,存儲轉化好的對象 
  52.     /* Finally call ZADD that will do the work for us. */ 
  53.     replaceClientCommandVector(c,argc,argv); 
  54.     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 命令都干了啥:

  • 參數提取和校驗
  • 將入參經緯度轉換為 52 位的 GEOHASH 值(Score)
  • 調用 ZADD 命令將 Member 及其對應的 Score 存入集合 Key 中。

GEORADIUS

使用方式

  1. 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 兩個只讀命令。

不過,在實際開發中筆者發現 在 java package Redis.clients.jedis.params.geo 的 GeoRadiusParam 參數類中并不包含 STORE 和 STORedisT 兩個參數選項。

在調用 GEORADIUS 時是否真的只查詢了主實例,還是進行了只讀封裝。感興趣的朋友可以自己研究下。

成功查詢后的返回值,不帶 WITH 限定,返回一個 member list,如:

  1. ["member1","member2","member3"

帶 WITH 限定,Member List 中每個 Member 也是一個嵌套 List,如:

  1.     ["member1", distance1, [longitude1, latitude1]] 
  2.     ["member2", distance2, [longitude2, latitude2]] 

源碼分析

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

  1. /* GEORADIUS key x y radius unit [WITHDIST] [WITHHASH] [WITHCOORD] [ASC|DESC
  2.  *                               [COUNT count] [STORE key] [STORedisT key
  3.  * GEORADIUSBYMEMBER key member radius unit ... options ... */ 
  4. void georadiusGeneric(client *c, int flags) { 
  5.     robj *key = c->argv[1]; 
  6.     robj *storekey = NULL
  7.     int stoRedist = 0; /* 0 for STORE, 1 for STORedisT. */ 
  8.  
  9. //根據key獲取有序集合 
  10.     robj *zobj = NULL
  11.     if ((zobj = lookupKeyReadOrReply(c, key, shared.null[c->resp])) == NULL || 
  12.         checkType(c, zobj, OBJ_ZSET)) { 
  13.         return
  14.     } 
  15.  
  16. //根據用戶輸入(經緯度/member)確認中心點經緯度 
  17.     int base_args; 
  18.     double xy[2] = { 0 }; 
  19.     if (flags & RADIUS_COORDS) { 
  20.         …… 
  21.     } 
  22.  
  23. //獲取查詢范圍距離 
  24.     double radius_meters = 0, conversion = 1; 
  25.     if ((radius_meters = extractDistanceOrReply(c, c->argv + base_args - 2, 
  26.                                                 &conversion)) < 0) { 
  27.         return
  28.     } 
  29.  
  30. //獲取可選參數 (withdist、withhash、withcoords、sort、count) 
  31.     int withdist = 0, withhash = 0, withcoords = 0; 
  32.     int sort = SORT_NONE; 
  33.     long long count = 0; 
  34.     if (c->argc > base_args) { 
  35.         ... ... 
  36.     } 
  37.  
  38. //獲取 STORE 和 STORedisT 參數 
  39.     if (storekey && (withdist || withhash || withcoords)) { 
  40.         addReplyError(c, 
  41.             "STORE option in GEORADIUS is not compatible with " 
  42.             "WITHDIST, WITHHASH and WITHCOORDS options"); 
  43.         return
  44.     } 
  45.  
  46. //設定排序 
  47.     if (count != 0 && sort == SORT_NONE) sort = SORT_ASC; 
  48.  
  49. //利用中心點和半徑計算目標區域范圍 
  50.     GeoHashRadius georadius = 
  51.         geohashGetAreasByRadiusWGS84(xy[0], xy[1], radius_meters); 
  52.  
  53. //對中心點及其周圍8個geohash網格區域進行查找,找出范圍內元素對象 
  54.     geoArray *ga = geoArrayCreate(); 
  55.     membersOfAllNeighbors(zobj, georadius, xy[0], xy[1], radius_meters, ga); 
  56.  
  57. //未匹配返空 
  58.     /* If no matching results, the user gets an empty reply. */ 
  59.     if (ga->used == 0 && storekey == NULL) { 
  60.         addReplyNull(c); 
  61.         geoArrayFree(ga); 
  62.         return
  63.     } 
  64.  
  65. //一些返回值的設定和返回 
  66.     …… 
  67.     geoArrayFree(ga); 

上文代碼中最核心的步驟有兩個,一是“計算中心點范圍”,二是“對中心點及其周圍 8 個 GEOHASH 網格區域進行查找”。

對應的是如下兩個函數:

  • geohashGetAreasByRadiusWGS84
  • membersOfAllNeighbors

我們依次來看:

①計算中心點范圍

// geohash_helper.c

  1. /* GEORADIUS key x y radius unit [WITHDIST] [WITHHASH] [WITHCOORD] [ASC|DESC
  2.  *                               [COUNT count] [STORE key] [STORedisT key
  3.  * GEORADIUSBYMEMBER key member radius unit ... options ... */ 
  4. void georadiusGeneric(client *c, int flags) { 
  5.     robj *key = c->argv[1]; 
  6.     robj *storekey = NULL
  7.     int stoRedist = 0; /* 0 for STORE, 1 for STORedisT. */ 
  8.  
  9. //根據key獲取有序集合 
  10.     robj *zobj = NULL
  11.     if ((zobj = lookupKeyReadOrReply(c, key, shared.null[c->resp])) == NULL || 
  12.         checkType(c, zobj, OBJ_ZSET)) { 
  13.         return
  14.     } 
  15.  
  16. //根據用戶輸入(經緯度/member)確認中心點經緯度 
  17.     int base_args; 
  18.     double xy[2] = { 0 }; 
  19.     if (flags & RADIUS_COORDS) { 
  20.         …… 
  21.     } 
  22.  
  23. //獲取查詢范圍距離 
  24.     double radius_meters = 0, conversion = 1; 
  25.     if ((radius_meters = extractDistanceOrReply(c, c->argv + base_args - 2, 
  26.                                                 &conversion)) < 0) { 
  27.         return
  28.     } 
  29.  
  30. //獲取可選參數 (withdist、withhash、withcoords、sort、count) 
  31.     int withdist = 0, withhash = 0, withcoords = 0; 
  32.     int sort = SORT_NONE; 
  33.     long long count = 0; 
  34.     if (c->argc > base_args) { 
  35.         ... ... 
  36.     } 
  37.  
  38. //獲取 STORE 和 STORedisT 參數 
  39.     if (storekey && (withdist || withhash || withcoords)) { 
  40.         addReplyError(c, 
  41.             "STORE option in GEORADIUS is not compatible with " 
  42.             "WITHDIST, WITHHASH and WITHCOORDS options"); 
  43.         return
  44.     } 
  45.  
  46. //設定排序 
  47.     if (count != 0 && sort == SORT_NONE) sort = SORT_ASC; 
  48.  
  49. //利用中心點和半徑計算目標區域范圍 
  50.     GeoHashRadius georadius = 
  51.         geohashGetAreasByRadiusWGS84(xy[0], xy[1], radius_meters); 
  52.  
  53. //對中心點及其周圍8個geohash網格區域進行查找,找出范圍內元素對象 
  54.     geoArray *ga = geoArrayCreate(); 
  55.     membersOfAllNeighbors(zobj, georadius, xy[0], xy[1], radius_meters, ga); 
  56.  
  57. //未匹配返空 
  58.     /* If no matching results, the user gets an empty reply. */ 
  59.     if (ga->used == 0 && storekey == NULL) { 
  60.         addReplyNull(c); 
  61.         geoArrayFree(ga); 
  62.         return
  63.     } 
  64.  
  65. //一些返回值的設定和返回 
  66.     …… 
  67.     geoArrayFree(ga); 

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

// geo.c

  1. //在9個hashBox中獲取想要的元素 
  2. int membersOfAllNeighbors(robj *zobj, GeoHashRadius n, double lon, double lat, double radius, geoArray *ga) { 
  3.     GeoHashBits neighbors[9]; 
  4.     unsigned int i, count = 0, last_processed = 0; 
  5.     int debugmsg = 0; 
  6.  
  7. //獲取9個搜索hashBox 
  8.     neighbors[0] = n.hash; 
  9.     …… 
  10.     neighbors[8] = n.neighbors.south_west; 
  11.  
  12. //在每個hashBox中搜索目標點 
  13.     for (i = 0; i < sizeof(neighbors) / sizeof(*neighbors); i++) { 
  14.         if (HASHISZERO(neighbors[i])) { 
  15.             if (debugmsg) D("neighbors[%d] is zero",i); 
  16.             continue
  17.         } 
  18.  
  19.     //剔除可能的重復hashBox (搜索半徑>5000KM時可能出現) 
  20.         if (last_processed && 
  21.             neighbors[i].bits == neighbors[last_processed].bits && 
  22.             neighbors[i].step == neighbors[last_processed].step) 
  23.         { 
  24.             continue
  25.         } 
  26.  
  27.     //搜索hashBox中滿足條件的對象     
  28.         count += membersOfGeoHashBox(zobj, neighbors[i], ga, lon, lat, radius); 
  29.         last_processed = i; 
  30.     } 
  31.     return count
  32.  
  33.  
  34. int membersOfGeoHashBox(robj *zobj, GeoHashBits hash, geoArray *ga, double lon, double lat, double radius) { 
  35. //獲取hashBox內的最大、最小geohash值(52位) 
  36.     GeoHashFix52Bits minmax
  37.     scoresOfGeoHashBox(hash,&min,&max); 
  38.  
  39. //根據最大、最小geohash值篩選zobj集合中滿足條件的點 
  40.     return geoGetPointsInRange(zobj, minmax, lon, lat, radius, ga); 
  41.  
  42.  
  43. int geoGetPointsInRange(robj *zobj, double mindouble maxdouble lon, double lat, double radius, geoArray *ga) { 
  44.  
  45. //搜索Range的參數邊界設置(即9個hashBox其中一個的邊界范圍) 
  46.     zrangespec range = { .min = min, .max = max, .minex = 0, .maxex = 1 }; 
  47.     size_t origincount = ga->used; 
  48.     sds member; 
  49.  
  50. //搜索集合zobj可能有ZIPLIST和SKIPLIST兩種編碼方式,這里以SKIPLIST為例,邏輯是一樣的 
  51.     if (zobj->encoding == OBJ_ENCODING_ZIPLIST) { 
  52.         …… 
  53.     } else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) { 
  54.         zset *zs = zobj->ptr; 
  55.         zskiplist *zsl = zs->zsl; 
  56.         zskiplistNode *ln; 
  57.  
  58.     //獲取在hashBox范圍內的首個元素(跳表數據結構,效率可比擬于二叉查找樹),沒有則返0 
  59.         if ((ln = zslFirstInRange(zsl, &range)) == NULL) { 
  60.             /* Nothing exists starting at our min.  No results. */ 
  61.             return 0; 
  62.         } 
  63.  
  64.     //從首個元素開始遍歷集合 
  65.         while (ln) { 
  66.             sds ele = ln->ele; 
  67.         //遍歷元素超出range范圍則break 
  68.             /* Abort when the node is no longer in range. */ 
  69.             if (!zslValueLteMax(ln->score, &range)) 
  70.                 break; 
  71.         //元素校驗(計算元素與中心點的距離) 
  72.             ele = sdsdup(ele); 
  73.             if (geoAppendIfWithinRadius(ga,lon,lat,radius,ln->score,ele) 
  74.                 == C_ERR) sdsfree(ele); 
  75.             ln = ln->level[0].forward
  76.         } 
  77.     } 
  78.     return ga->used - origincount; 
  79.  
  80. int geoAppendIfWithinRadius(geoArray *ga, double lon, double lat, double radius, double score, sds member) { 
  81.     double distance, xy[2]; 
  82.  
  83. //解碼錯誤, 返回error 
  84.     if (!decodeGeohash(score,xy)) return C_ERR; /* Can't decode. */ 
  85.  
  86. //最終距離校驗(計算球面距離distance看是否小于radius) 
  87.     if (!geohashGetDistanceIfInRadiusWGS84(lon,lat, xy[0], xy[1], 
  88.                                            radius, &distance)) 
  89.     { 
  90.         return C_ERR; 
  91.     } 
  92.  
  93. //構建并返回滿足條件的元素 
  94.     geoPoint *gp = geoArrayAppend(ga); 
  95.     gp->longitude = xy[0]; 
  96.     gp->latitude = xy[1]; 
  97.     gp->dist = distance; 
  98.     gp->member = member; 
  99.     gp->score = score; 
  100.     return C_OK; 

算法小結

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

  • 參數提取和校驗。
  • 利用中心點和輸入半徑計算待查區域范圍。這個范圍參數包括滿足條件的最高的 GEOHASH 網格等級(精度)以及對應的能夠覆蓋目標區域的九宮格位置(后續會有詳細說明)。
  • 對九宮格進行遍歷,根據每個 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 本身基于內存的存儲特性,在實際使用過程中有非常高的運行效率。

作者:萬汨

簡介:餓了么資深開發工程師。iOS,Go,Java 均有涉獵。目前主攻大數據開發。喜歡騎行、爬山。

 

責任編輯:武曉燕 來源: 餓了么物流技術團隊
相關推薦

2021-07-06 08:41:54

RedisGeo 類型數據類型

2015-07-15 13:18:27

附近的人開發

2024-05-17 16:04:45

iOS 18蘋果

2023-11-20 08:13:01

2022-10-30 10:18:09

Geohash算法Haversine

2010-07-16 16:28:46

2021-07-22 10:25:07

JS驗證碼前端

2022-05-11 07:41:31

Python驗證碼

2021-04-27 16:04:26

人工智能Python 語音識別

2015-05-04 16:22:39

2023-03-02 06:00:00

2021-02-01 16:55:13

iPhoneTouch ID蘋果

2021-12-14 15:20:37

Python微信代碼

2020-05-11 17:00:30

點贊MySQLRedis

2009-07-03 09:44:39

實現RSS功能JSP技術

2020-08-12 09:55:07

附近的人數據庫MySQL

2020-04-16 12:42:42

附近的人共享單車App

2018-10-19 11:55:36

數據庫Redis命令

2020-07-02 16:40:40

MySQLRedis數據庫

2012-12-28 14:32:34

Android開發Handler異步處理
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 宅男伊人 | 精品久久久999 | 中文字幕黄色大片 | 伊人免费视频二 | 91视频a| 日韩一区二区三区四区五区 | 国产精品久久国产精品 | 动漫www.被爆羞羞av44 | 国产精品国产精品国产专区不卡 | 美女爽到呻吟久久久久 | 91视频三区 | 成人影视网 | 成人亚洲性情网站www在线观看 | 亚洲成人av在线播放 | 成人性视频在线 | 亚洲天堂影院 | 久久久久国产 | 亚洲视频免费观看 | 久草在线在线精品观看 | 久久一 | 日韩精品二区 | 在线播放中文字幕 | 日韩在线综合 | 青娱乐国产 | 欧美一区二区三区在线视频 | 久久精品91久久久久久再现 | 羞羞的视频免费看 | 亚洲视频精品 | 国产亚洲精品精品国产亚洲综合 | 久久99精品久久久久久 | 国产一区二区成人 | 精品丝袜在线 | 色婷婷久久久久swag精品 | 本道综合精品 | 性一交一乱一透一a级 | 一级日韩 | 视频一区 亚洲 | 久久久青草婷婷精品综合日韩 | 欧美一级二级三级视频 | 天天草天天干 | 国产精品不卡一区 |