Redis學(xué)習(xí)之旅~基礎(chǔ)應(yīng)用篇

這一年以來(lái),寫(xiě)了太多的業(yè)務(wù)代碼。是時(shí)候要總結(jié)一下自己的積累了。本文是redis深度歷險(xiǎn)的讀書(shū)筆記,做個(gè)記錄以及分享給大家。

docker redis redis插件加載

數(shù)據(jù)結(jié)構(gòu)

字符串

  • 字符串是一個(gè)字符數(shù)組

  • 常見(jiàn)用途就是信息JSON序列化成為字符串之后,存入redis,取信息會(huì)經(jīng)過(guò)一次反序列化

  • 字符串是動(dòng)態(tài)字符串,可以進(jìn)行擴(kuò)容;當(dāng)字符串長(zhǎng)度小于1m的時(shí)候,擴(kuò)容是加倍。當(dāng)大于1m,每次擴(kuò)容就增加1m。字符串最大的長(zhǎng)度為512m

  • 使用字符串計(jì)數(shù)的時(shí)候,單個(gè)值的最大值為signed long,超過(guò)就會(huì)報(bào)錯(cuò)

  • 常用命令

    set name a
    
    get name
    
    exists a
    
    del a
    
    mset a 1 b 2
    
    mget a b
    
    expire a 5
    
    setex a 5 1
    
    setnx a 1
    
    incr a
    
    incrby a 10
    

列表

  • 相當(dāng)于java中的LinkedList,一個(gè)雙向鏈表。插入和刪除操作快,但是索引定位很慢。

    
    這里幫助大家回憶一下普通節(jié)點(diǎn)刪除操作
    
    p.pre.next = p.next
    
    p.next.pre = p.pre
    
    p = null
    
    不需要對(duì)節(jié)點(diǎn)進(jìn)行移動(dòng)
    
  • 列表可以作為隊(duì)列,右進(jìn)左出即可

    rpush books java python golang kotlin
    
    4
    
    llen books
    
    4
    
    lpop books
    
    java
    
    
  • 列表可以作為棧,右進(jìn)右出即可。就是,調(diào)整了隊(duì)列使用的方向

    rpush books java python golang kotlin
    4
    llen books
    4
    rpop books
    kotlin
    
  • 使用隊(duì)列會(huì)存在的慢操作有g(shù)et(index)方法,需要遍歷整個(gè)鏈表,性能為O(n)

    rpush books java python golang kotlin
    4
    lindex 1            # 時(shí)間為O(n)
    lindex books 1
    "python"
    lrange books 0 -1
    "java"
    "python"
    "golang"
    "kotlin"
    ltrim books 1 -1      # 設(shè)置長(zhǎng)度為3的數(shù)組,區(qū)間外的全部被清除
    OK
    lrange books 0 -1    # 獲取所有元素
    "python"
    "golang"
    "kotlin"
    llen books
    3
    ltrim books 1 0        # 刪除所有元素
    OK
    
    
  • 快速列表:在列表元素較少的時(shí)候,會(huì)使用一塊連續(xù)的內(nèi)存,叫ziplist(壓縮列表)。當(dāng)數(shù)據(jù)量較多的時(shí)候,就要變成quicklist(快速列表),多個(gè)ziplist使用雙向指針串起來(lái)使用。


// 快速列表

struct quicklist {

  quicklistNode* head;

  quicklistNode* tail;

  long count; // 元素總數(shù)

  int nodes; // ziplist 節(jié)點(diǎn)的個(gè)數(shù)

  int compressDepth; // LZF 算法壓縮深度

  ...

}

// 快速列表節(jié)點(diǎn)

struct quicklistNode {

  quicklistNode* prev;

  quicklistNode* next;

  ziplist* zl; // 指向壓縮列表

  int32 size; // ziplist 的字節(jié)總數(shù)

  int16 count; // ziplist 中的元素?cái)?shù)量

  int2 encoding; // 存儲(chǔ)形式 2bit,原生字節(jié)數(shù)組還是 LZF 壓縮存儲(chǔ)

}

struct ziplist_compressed {

  int32 size;

  byte[] compressed_data;

}

struct ziplist {

  ...

}

Hash字典

  • 內(nèi)部實(shí)現(xiàn)結(jié)構(gòu)就是數(shù)組+鏈表的方式進(jìn)行實(shí)現(xiàn)。相同hash值的元素使用鏈表串聯(lián)起來(lái)。java的鏈表數(shù)量超過(guò)6個(gè)會(huì)變成紅黑樹(shù),不知道redis的會(huì)不會(huì)有這個(gè)查詢(xún)優(yōu)化。

  • redis里面的值就只能是字符串,便于比較。不像java的hashmap可以存儲(chǔ)對(duì)象,equals方法的編寫(xiě)可謂五花八門(mén)

  • java的hashmap在進(jìn)行rehash的時(shí)候,需要一次性進(jìn)行rehash,是個(gè)耗時(shí)操作,多線程還會(huì)出現(xiàn)環(huán)。redis的是漸進(jìn)式rehash。rehash的時(shí)候,會(huì)保留兩個(gè)hash結(jié)構(gòu),查詢(xún)時(shí)候會(huì)查詢(xún)兩個(gè)hash結(jié)構(gòu),在后續(xù)的定時(shí)任務(wù)和hash指令操作的時(shí)候,漸進(jìn)地將舊的內(nèi)容遷移到新的hash結(jié)構(gòu)中。當(dāng)內(nèi)容遷移完畢,刪除舊的hash結(jié)構(gòu)

    hset books java "think in java"
    1
    hset books golang "concurrency in go"
    1
    hset books python "python cookbook"
    1
    hgetall books  # 返回所有entryies,key和value間隔返回
    "java"
    "think in java"
    "golang"
    "concurrency in go"
    "python"
    "python cookbook"
     hlen books
     3
    hget books java
    "think in java"
    hset books java "my java book"
    0
    hmset books java "java" golang "golang"
    ok
    hset user-lixinkun age 25
    1
    hincrby user-lixinkun 1
    26
    

set

  • 相當(dāng)于java的hashset,確保唯一性

    sadd books java
    1
    sadd books java
    0
    sadd books python golang
    smembers books
    java
    golang
    python    #順序可能被打亂,輸出結(jié)果是無(wú)序的
    

Zset 有序列表

  • 類(lèi)似于java的sortedset和hashmap的結(jié)合體。具備set的唯一性原理,也可以給每個(gè)值賦一個(gè)score,代表它的排序權(quán)重。它的內(nèi)部實(shí)現(xiàn)是一個(gè)跳躍列表。貌似java也有這個(gè)數(shù)據(jù)結(jié)構(gòu)

  • 內(nèi)部排序通過(guò)skiplist實(shí)現(xiàn)。zset要支持睡覺(jué)的插入和刪除,不能使用數(shù)組來(lái)表示。普通的鏈表數(shù)據(jù)結(jié)構(gòu),有新元素插入的時(shí)候,需要進(jìn)行定位,這樣才能保證是有序的。通常會(huì)通過(guò)二分查找來(lái)確定位置,但是只有數(shù)組才支持快速查找。這時(shí)候就需要一個(gè)多層級(jí)的列表結(jié)構(gòu),最下面一層的元素都是串聯(lián)起來(lái)的。某個(gè)元素存在于所有的層。定位插入點(diǎn)的時(shí)候,先在頂層進(jìn)行定位,然后下潛到下一層,然后一直到最底下一層。redia采用隨機(jī)策略來(lái)決定元素可以兼職到第幾層。L0層的概率是100%,L1的是50%。這樣去類(lèi)推

  • 數(shù)據(jù)結(jié)構(gòu),具體介紹鏈接redis跳躍列表介紹

    /* ZSETs use a specialized version of Skiplists */
    typedef struct zskiplistNode {
    //成員對(duì)象
    robj *obj;
    //分值
    double score;
    //后退指針
    struct zskiplistNode *backward;
    //層
      struct zskiplistLevel {
          struct zskiplistNode *forward;//前進(jìn)指針
          unsigned int span;//跨度
      } level[];
    } zskiplistNode;
    typedef struct zskiplist {
    //表頭節(jié)點(diǎn)和表尾節(jié)點(diǎn)
    struct zskiplistNode *header, *tail;
    //表中節(jié)點(diǎn)的的數(shù)量
    unsigned long length;
    //表中層數(shù)最大的節(jié)點(diǎn)層數(shù)
    int level;
    } zskiplist;
    

容器的通用規(guī)則

  • create if not exists.如果容器不存在,則先創(chuàng)建容器。

  • drop if no elements.如果容器沒(méi)有元素了,就立即刪除該容器,釋放內(nèi)存。

  • 容器的過(guò)期時(shí)間是針對(duì)整個(gè)對(duì)象的,hash的過(guò)期是整個(gè)進(jìn)行過(guò)期,而不是單個(gè)元素

分布式鎖

  • 對(duì)于不是原子性的操作,就需要通過(guò)分布式鎖來(lái)保證只有一個(gè)線程可以獲取到鎖。具體使用的是setnx和del命令。鎖處理完了以后,就要被釋放。中間需要解決的問(wèn)題就是,如果業(yè)務(wù)邏輯執(zhí)行之間,出現(xiàn)了異常,可能導(dǎo)致del無(wú)法執(zhí)行,整個(gè)業(yè)務(wù)癱瘓。

  • 或許,可以加一個(gè)過(guò)期時(shí)間,5s以后自動(dòng)釋放。但是邏輯還有問(wèn)題的,如果expire的時(shí)候,命令丟失了,也會(huì)死鎖。出現(xiàn)這種問(wèn)題的原因就是setnx和expire兩個(gè)命令不是原子性的。

  • 在redis2.8的版本中,作者加入了set指令的擴(kuò)展餐廚,setnx和expire指令可以一起執(zhí)行了,指令是原子性的。

加鎖
set lock:book true ex 5 nx
//釋放鎖
del lock:book
  • 超時(shí)問(wèn)題是redis分布式鎖不能解決的問(wèn)題,如果加鎖和釋放鎖之間的邏輯太長(zhǎng),超出了鎖的超時(shí)時(shí)限,那么第二個(gè)線程可能獲得redis分布式鎖。所以,redis分布式鎖不要用于較長(zhǎng)時(shí)間的任務(wù)

  • 可重入性。java里面有一個(gè)ReentrantLock。Redis分布式鎖如果要支持重入,需要對(duì)客戶(hù)端的set方法進(jìn)行封裝,使用線程的ThreadLocal遍歷存儲(chǔ)當(dāng)前持有鎖的計(jì)數(shù)。

  • 下面代碼redis分布式鎖的使用,這里鏈接是錯(cuò)誤案例介紹redis分布式鎖使用與錯(cuò)誤示例


//錯(cuò)誤使用就是使用setnx和expire兩個(gè)指令

/**

* 嘗試獲取分布式鎖
* @param jedis Redis客戶(hù)端
* @param lockKey 鎖
* @param requestId 請(qǐng)求標(biāo)識(shí)
* @param expireTime 超期時(shí)間
* @return 是否獲取成功
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
    String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
    if (LOCK_SUCCESS.equals(result)) {
        return true;
    }
    return false;
}
  • java版本的可重入鎖,核心就是用threadlocal去記錄每個(gè)線程,某個(gè)key的鎖計(jì)數(shù)
public class RedisWithReentrantLock{
  //線程鎖計(jì)數(shù)器8
  private ThreadLocal<Map<String,Integer>> lockers = new ThreadLocal<>();

  private Jedis jedis;

  //內(nèi)部使用的lock
  private boolean _lock(String key) {
    return jedis.set(key, "", "nx", "ex", 5L) != null;
  }

  //內(nèi)部使用的unlock
  private void _unlock(String key) {
    jedis.del(key);
  }

  //獲取計(jì)數(shù)器
  private Map<String, Integer> currentLockers() {
    Map<String, Integer> refs = lockers.get();
    if (refs != null) {
      return refs;
    }
    lockers.set(new HashMap<>());
    return lockers.get();
  }
  //可重入加鎖
  public boolean lock(String key) {
    Map<String, Integer> refs = currentLockers();
    Integer refCnt = refs.get(key);
    if (refCnt != null) {
      refs.put(key, refCnt + 1);
      return true;
    }
    boolean ok = _lock(key);
    if (!ok) {
      return false;
    }
    refs.put(key, 1);
    return true;
  }
  //可重入釋放鎖
  public boolean unlock(String key) {
    Map<String, Integer> refs = currentLockers();
    Integer refCnt = refs.get(key);
    if (refCnt == null) {
      return false;
    }
    refCnt = -1;
    if (refCnt > 0) {
      refs.put(key, refCnt);
    } else {
      refs.remove(key);
      _unlock(key);
    }
    return true;
  }
}

redis的消息隊(duì)列,只做特性介紹,公司實(shí)際使用rocketmq

  • 使用簡(jiǎn)單,只有一個(gè)消費(fèi)者

  • 沒(méi)有ack保證,如果對(duì)消息可靠性有極高的要求,推薦使用rocketmq的事務(wù)消息

  • 使用list作為異步消息的發(fā)送結(jié)構(gòu),rpush,rpop

延時(shí)隊(duì)列的實(shí)現(xiàn)

  • 延時(shí)隊(duì)列可以使用zset實(shí)現(xiàn),將消息序列化成為一個(gè)字符串,消息的到期時(shí)間為score。然后使用多個(gè)線程去輪詢(xún)這個(gè)隊(duì)列。使用多線程是為了保障可用性。一個(gè)線程掛了,還有其他線程可以進(jìn)行處理

位圖,節(jié)約存儲(chǔ)空間

  • 平時(shí)進(jìn)行開(kāi)發(fā)的時(shí)候,要對(duì)一些bool類(lèi)型的數(shù)據(jù)進(jìn)行存取,如用戶(hù)一年的簽到記錄。如果使用key/value方式進(jìn)行存取,就要記錄365個(gè),當(dāng)用戶(hù)量上億的時(shí)候,需要的存儲(chǔ)結(jié)構(gòu)是非常驚人的。

  • redis提供了位圖數(shù)據(jù)結(jié)構(gòu),每個(gè)用戶(hù)每天的簽到就只占據(jù)一個(gè)位。365天大概需要46個(gè)字節(jié),大大節(jié)約空間

  • 位圖就是byte數(shù)組,可以使用getbit和setbit進(jìn)行存儲(chǔ),如果字節(jié)是不可以打印字符,redis會(huì)展示其16進(jìn)制。實(shí)際來(lái)操作一波

零存整取的方式,一個(gè)個(gè)bit去設(shè)置,然后一次性拿出值
通過(guò)python獲取字母`h`的hash值為0b1101000
setbit s 1 1
setbit s 2 1
setbit s 4 1
get s
"h"      # 這樣就得到了h的值

零存零取的方式,就是對(duì)單個(gè)bit進(jìn)行操作,沒(méi)有設(shè)置的位就是默認(rèn)為0
set bit w 1 1
get bit w 0
0

整存零取
set w h
getbit w 1
1

不可打印字符
setbit x 0 1
setbit x 1 1
get x
"\xc0"
  • 位圖的統(tǒng)計(jì)和查找:redis提供了對(duì)位圖的基本統(tǒng)計(jì)算
 set w hello
 ok
 bitcount w
 21
 bitcount w 0 0
 3
 bitcount w 0 1  # 統(tǒng)計(jì)1的數(shù)量
 7
 bitpos w 0  # 第一個(gè)0
 0

 bitpos w 1 1 1 # 第二個(gè)字符算起,第一個(gè)1位
 魔術(shù)指令bitfield
 bitfield w get u4 0

高級(jí)數(shù)據(jù)結(jié)構(gòu)

HyberLogLog,提供不精確的統(tǒng)計(jì)數(shù)據(jù)

  • 頁(yè)面的uv,不能使用一個(gè)key的incr,因?yàn)橐M(jìn)行去重

  • 使用zset也可以做到這個(gè),但是消耗空間太大

  • HyperLogLog的標(biāo)準(zhǔn)誤差是0.81%。對(duì)于不精確的統(tǒng)計(jì),可以使用這個(gè)數(shù)據(jù)結(jié)構(gòu)

  • 提供pdadd,pfcount兩個(gè)指令,pf是Philippe Flajolet教授的名字縮寫(xiě),這個(gè)數(shù)據(jù)結(jié)構(gòu)是他發(fā)明的。這個(gè)數(shù)據(jù)結(jié)構(gòu)占用的內(nèi)存是12k,使用了2^14個(gè)桶去進(jìn)行計(jì)算


pfadd book java
1
pfadd book java
0
pfadd book python
1
pfadd book python
pfcount book
2

布隆過(guò)濾器Bloom Filter

  • 這個(gè)數(shù)據(jù)結(jié)構(gòu)主要實(shí)現(xiàn)去重的功能

  • 場(chǎng)景使用:廣告推送的時(shí)候,如果我們看過(guò)了一條廣告,下一條推送的時(shí)候,要對(duì)看過(guò)的廣告進(jìn)行去重。如果使用關(guān)系型數(shù)據(jù)庫(kù),每次推送都要去數(shù)據(jù)庫(kù)查詢(xún),這樣數(shù)據(jù)庫(kù)很難抗住這個(gè)壓力。況且,瀏覽記錄會(huì)隨著時(shí)間線性增長(zhǎng),空間也跟不上

  • 布隆過(guò)濾器會(huì)有一定的誤判概率。這可以比喻成為一個(gè)不精確的set結(jié)構(gòu),調(diào)用contains的時(shí)候,有可能誤判。如果說(shuō)這個(gè)值不存在,那就肯定不存在。如果說(shuō)這個(gè)值存在,優(yōu)肯是誤判,可能,兩者長(zhǎng)得相像?這樣的話(huà),對(duì)于實(shí)際業(yè)務(wù)也沒(méi)有影響,我們可以保證推送給用戶(hù)的都是最新的內(nèi)容,只有極少數(shù)用戶(hù)沒(méi)看過(guò)的內(nèi)容會(huì)被誤判。經(jīng)過(guò)測(cè)試,誤判率大概在1%左右。調(diào)整參數(shù)可以?xún)?yōu)化這個(gè)誤判率

  • 布隆過(guò)濾器作為redis的一個(gè)插件,需要版本為4.0以后,才能使用。

  • 基本指令為bf.add,bf.exists,bf.madd(批量),安裝docker,然后去重新安裝redis,帶這個(gè)插件的版本。centos7安裝docker方法,安裝好以后再安裝插件,使用docker啟動(dòng)redis

  • 用戶(hù)可以通過(guò)bf.reserve命令顯式制定錯(cuò)誤率,key和初始空間。錯(cuò)誤率越低,占用空間越大。初始值越大,空間越大,所以,在使用的時(shí)候,需要準(zhǔn)確預(yù)估一下元素的大小。

  • 布隆過(guò)濾器是一個(gè)大型的位數(shù)組。向布隆過(guò)濾器添加key的時(shí)候,會(huì)使用多個(gè)hash函數(shù)對(duì)key進(jìn)行hash,算得一個(gè)整數(shù)索引。然后對(duì)位數(shù)組的長(zhǎng)度取模運(yùn)算得到一個(gè)位置,每個(gè)hash函數(shù)都得到一個(gè)不同的位置,再把這幾個(gè)位置都設(shè)置為1,完成了add操作。巧妙地共享可多個(gè)key的存儲(chǔ)空間。

  • 為何會(huì)出現(xiàn)誤判呢?就是由于判斷key是否存在的時(shí)候,如果某個(gè)位是0,那就一定不存在。但是,如果各個(gè)位置都是1的話(huà),也不能說(shuō)明是存在的,有可能是其他key存在導(dǎo)致。如果位數(shù)組比較稀疏,出現(xiàn)誤判的概率就低了。所以,使用的時(shí)候,不要讓實(shí)際的元素大于初始值。如果大于的時(shí)候,要對(duì)布隆過(guò)濾器進(jìn)行重建。

  • set存儲(chǔ)的是所有的元素,布隆過(guò)濾器是存儲(chǔ)元素的指紋(hash)??臻g節(jié)省還是比較明顯的.

  • docker加載插件的命令,docker run -p 6379:6379 --name redis-redisbloom redislabs/rebloom:latest

  • docker exec -it redis-redisbloom bash

bf.add book java
1
bf.add book java   #再次加入java,就被排出了
0
bf.exists book python
0

redis限流

  • 后端屏蔽用戶(hù)的過(guò)于頻繁的請(qǐng)求,比如1秒內(nèi)不能請(qǐng)求超過(guò)5次。這個(gè)限流存在一個(gè)滑動(dòng)時(shí)間窗口,zset的score圈出一個(gè)時(shí)間窗口,我們保留這個(gè)時(shí)間窗口,對(duì)于窗口外面的數(shù)據(jù),則可以統(tǒng)統(tǒng)砍掉。zset的value可以為毫秒時(shí)間戳。

public class SimpleRateLimiter{
  private Jedis jedis;

  //用戶(hù)請(qǐng)求是否允許

  //使用樂(lè)觀鎖

  //value保證唯一性就好

  public boolean isActionAllow(String userId, String actionKey, int period, int maxCount) {
    String key = String.format("hist:%s:%s", userId, actionKey);
    long now = System.currentTimeMillis();
    Pipeline pipe = jedis.pipelined();
    pipe.multi();
    pipe.zadd(key, now, "", now);

    //截取
    pipe.zremrangeByScore(key, 0, now - period * 1000);
    Response<Long> count = pipe.zcard(key);
    pipe.expire(key, period + 1);
    pipe.exec();
    pipe.close();
    return count.get() <= maxCount;
  }
}
  • 漏斗限流,靈感來(lái)源于漏斗,就是漏斗有一個(gè)容量,也有一個(gè)消耗速率。當(dāng)灌水速率小于漏水速率,漏斗超過(guò)容量以后。灌水行為就要丟棄或者暫停

//單機(jī)版的漏斗限流

public class FunnelRateLimiter{
  class Funnel{
    //容量
    int capacity;
    //漏水速率
    float leakingRate;
    //剩余空間
    int leftQuota;
    //上次觸發(fā)時(shí)間
    long leakingTs;
  }

  //....省略構(gòu)造方法

  void makeSpace() {
    long now = System.currentTimeMillis();
    long deltaTs = nowTs - leakingTs;
    int deltaQuota = (int)(deltaTs * leakingRate);
    //如果時(shí)間間隔太久,可能會(huì)導(dǎo)致int溢出
    if (deltaQuota < 0) {
      //重設(shè)
      this.leftQuota = capacity;
      this.leakingTs = 0;
      return;
    }

    if (deltaQuota < 1) {
      return;
    }

    this.leftQuota += leftQuota;
    this.leakingTs = nowTs;
    if (this.leftQuota > this.capacity) {
      this.leftQuota = this.capacity;
    }

    //是否可以接收請(qǐng)求

    boolean watering(int quota) {
      makeSpace();
      if (leftQuota >= quota) {
        this.leftQuota -= quota;
        return true;
      }
      return false;
    }
  }

  private Map<String, Funnel> funnels = new HashMap<>();
  public boolean isActionAllow(String userId, String actionKey, int capacity, fload leakingRate) {
    String key = String.format("funnels:%s:%s", userId, actionKey);
    Funnel funnel = funnels.get(key);
    if (funnel == null) {
      funnel = new Funnel(capacity, leakingRate);
      funnels.put(key, funnel);
    }
    return funnel.watering(1);
  }
}

  • 這里是redis提供的分布式限流,redis4.0提供了redis-cell。這是一個(gè)差價(jià),需要去github下載redis-cell的源碼解壓以后。進(jìn)入redis-cli后,執(zhí)行redis的 module load命令即可加載
wget https://github.com/brandur/redis-cell/releases/download/v0.2.4/redis-cell-v0.2.4-x86_64-unknown-linux-gnu.tar.gz

tar -xzvf redis-cell-v0.2.4-x86_64-unknown-linux-gnu.tar.gz

解壓后得到libredis_cess.so

redis-cli

module load /data/libredis_cell.so

我這里是放到了data目錄下面,運(yùn)行redis后加載就行了

//各個(gè)參數(shù)    key    容量          計(jì)算漏斗速率              可選參數(shù)

cl.throttle <key> <max_burst> <count per period> <period> [<quantity>]

cl.throttle book 15 30 60 1  # 這個(gè)表示容量為15,2秒一個(gè)漏水速率(30/60)

0          # 0 允許 -1 拒絕

16        # 漏斗容量

15        # 剩余空間

-1        # 當(dāng)?shù)谝粋€(gè)參數(shù)為-1師,這里的值表示需要經(jīng)過(guò)多少時(shí)間以后才能再試

2          # 多久時(shí)間以后,漏斗可以完全空出來(lái)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容