主頁 > 軟體設計 > 一個簡單的字串,為什么 Redis 要設計的如此特別

一個簡單的字串,為什么 Redis 要設計的如此特別

2021-01-12 12:21:51 軟體設計

一個簡單的字串,為什么 Redis 要設計的如此特別

  • 五種基本資料型別之字串物件
    • 二進制安全字串
    • 什么是二進制安全的字串
    • sds 空間分配策略
      • 空間預分配
      • 惰性空間釋放
    • sds 和 C 語言字串區別
  • sds 是如何被存盤的
    • type 屬性
    • encoding 屬性
      • embstr 編碼為什么從 39 位修改為 44 位
      • embstr 編碼和 raw 編碼的區別
  • 總結

本文GitHub已收錄:https://zhouwenxing.github.io/

Redis 中支持的資料型別到 5.0.5 版本,一共有 9 種,分別是:

  • 1、Binary-safe strings(二進制安全字串)
  • 2、Lists(串列)
  • 3、Sets(集合)
  • 4、Sorted sets(有序集合)
  • 5、Hashes(哈希)
  • 6、Bit arrays (or simply bitmaps)(位圖)
  • 7、HyperLogLogs
  • 8、 geospatial
  • 9、Streams

雖然這里列出了 9 種,但是基礎型別就是前面 5 種,后面的 4 種是基于前面 5 種基本型別及特定的演算法來實作的特殊型別,

而在 5 種基礎型別之中,又尤其以字串型別最為常用,且 key 值只能為字串物件,所以要想深入的了解 Redis 的特性,字串物件是首先需要學習的,

五種基本資料型別之字串物件

Redis 當中有五種基礎資料型別,而字串物件又是最重要最常用的一種型別,

二進制安全字串

Redis 是基于 C 語言進行開發的,而 C 語言中的字串是二進制不安全的,所以 Redis 就沒有直接使用 C 語言的字串,而是自己撰寫了一個新的資料結構來表示字串,這種資料結構稱之為:簡單動態字串(Simple dynamic string),簡稱 SDS

什么是二進制安全的字串

C 語言中,字串采用的是一個 char 陣列(柔性陣列)來存盤字串,而且字串必須要以一個空字串 \0 來結尾,而且字串并不記錄長度,所以如果想要獲取一個字串的長度就必須遍歷整個字串,直到遇到第一個 \0 為止(\0 不會計入字串長度),故而獲取字串長度的時間復雜度為 O(n)

正因為 C 語言中是以遇到的第一個空字符 \0 來識別是否到了字串末尾,因此其只能保存文本資料,不能保存圖片,音頻,視頻和壓縮檔案等二進制資料,否則可能出現字串不完整的問題,所以其是二進制不安全的,

Redis 中為了實作二進制安全的字串,對原有 C 語言中的字串實作做了改進,如下所示就是一個舊版本的 sds 字串的結構定義:

struct sdshdr{
  int len;//記錄buf陣列已使用的長度,即SDS的長度(不包含末尾的'\0')
  int free;//記錄buf陣列中未使用的長度
  char buf[];//位元組陣列,用來保存字串
}

經過改進之后,如果想要獲取 sds 的長度不用去遍歷 buf 陣列了,直接讀取 len 屬性就可以得到長度,時間復雜度一下就變成了 O(1),而且因為判斷字串長度不再依賴空字符 \0,所以其能存盤圖片,音頻,視頻和壓縮檔案等二進制資料,不用擔心讀取到的字串不完整,

需要注意的是,sds 依然遵循了 C 語言字串以 \0 結尾的慣例,這么做是為了方便復用 C 語言字串原生的一些API,換言之就是在 C 語言中會以碰到的第一個 \0 字符當做當前字串物件的結尾,所以如果一些二進制資料就會可能出現讀取字串不完整的現象,而 sds 會以長度來判斷是否到字串末尾,

Redis 3.2 之后的版本,Redissds 又做了優化,按照存盤空間的大小拆分成為了 sdshdr5sdshdr8sdshdr16sdshdr32sdshdr64,分別用來存盤大小為:32 位元組(25 次方),256 位元組(28 次方),64KB216 次方),4GB 大小(232 次方)以及 264 次方大小的字串(因為目前版本 keyvalue 都限制了最大 512MB,所以 sdshdr64 暫時并未使用到), sdshdr5 只被應用在了 Redis 中的 key 中,value 中不會被使用到,因為sdshdr5和其他型別也不一樣,其并沒有存盤未使用空間,所以其是比較適用于使用大小固定的場景(比如 key 值):

在這里插入圖片描述

任意選擇其中一種資料型別,其欄位代表含義如下:

struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; //已使用空間大小
    uint8_t alloc; //總共申請的空間大小(包括未使用的)
    unsigned char flags; //用來表示當前sds型別是sdshdr8還是sdshdr16等
    char buf[]; //真實存盤字串的位元組陣列
};

可以看到相比較于 Redis 3.2 版本之前的 sds 主要是修改了 free 屬性然后新增了一個 flags 標記來區分當前的 sds 型別,

sds 空間分配策略

C 語言中因為字串內部沒有記錄長度,所以如果擴充字串的時候非常容易造成緩沖區溢位(buffer overflow)

請看下面這張圖,假設下面這張圖就是記憶體里面的連續空間,可以很明顯的看到,此時 wolfRedis 兩個字串之間只有三個空位,那么這時候如果我們要將 wolf 字串修改為 lonelyWolf,那么就需要 6 個空間,這時候下面這個空間是放不下的,所以必須要重新申請空間,但是假如說程式員忘了申請空間,或者說申請到的空間依然不夠,那么就會出現后面的 Redis 字串中的 Red 被覆寫了:
在這里插入圖片描述

同樣的,假如要縮小字串的長度,那么也需要重新申請釋放記憶體,否則,字串一直占據著未使用的空間,會造成記憶體泄露

C 語言避免快取區溢位和記憶體泄露完全依賴于人為,很難把控,但是使用 sds 就不會出現這兩個問題,因為當我們操作 sds時,其內部會自動執行空間分配策略,從而避免了上述兩種情況的出現,

空間預分配

空間預分配指的是當我們通過 apisds 進行擴展空間的時候,假如未使用空間不夠用,那么程式不僅會為 sds 分配必須要的空間,還會額外分配未使用空間,未使用空間分配大小主要有兩種情況:

  • 1、假如擴大長度之后的 len 屬性小于等于 1MB (即 1024 * 1024),那么就會同時分配和 len 屬性一樣大小的未使用空間(此時 buf 陣列已使用空間 = 未使用空間),
  • 2、假如擴大長度之后的 len 屬性大于 1MB,那么就會分配 1MB 未使用空間大小,

執行空間預分配策略的好處是提前分配了未使用空間備用后,就不需要每次增大字串都需要分配空間,減少了記憶體重分配的次數,

惰性空間釋放

惰性空間釋放指的是當我們需要通過 api 減小 sds 長度的時候,程式并不會立即釋放未使用的空間,而只是更新 free 屬性的值,這樣空間就可以留給下一次使用,而為了防止出現記憶體溢位的情況,sds 單獨提供給了 api 讓我們在有需要的時候去真正的釋放記憶體,

sds 和 C 語言字串區別

下面表格中列舉了 Redis 中的 sdsC 語言中實作的字串的區別:

C 字串SDS
只能保存文本類不含空字串 \0 資料可以保存文本或者二進制資料,允許包含空字串 \0
獲取字串長度的復雜度為 O(n)獲取字串長度的復雜度為 O(1)
操作字串可能會造成緩沖區溢位不會出現緩沖區溢位情況
修改字串長度 N 次,必然需要 N次記憶體重分配修改字串長度 N 次,最多需要 N 次記憶體重分配
可以使用 C 字串相關的所有函式可以使用 C 字串相關的部分函式

sds 是如何被存盤的

Redis 中所有的資料型別都是將對應的資料結構再進行了再一次包裝,創建了一個字典物件來存盤的,sds也不例外,每次創建一個 key-value 鍵值對,Redis 都會創建兩個物件,一個是鍵物件,一個是值物件,而且需要注意的是Redis 中,值物件并不是直接存盤,而是被包裝成 redisObject 物件,并同時將鍵物件和值物件通過 dictEntry 物件進行封裝,如下就是一個 dictEntry 物件:

typedef struct dictEntry {
    void *key;//指向key,即sds
    union {
        void *val;//指向value
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;//指向下一個key-value鍵值對(哈希值相同的鍵值對會形成一個鏈表,從而解決哈希沖突問題)
} dictEntry;

redisObject 物件的定義為:

typedef struct redisObject {
    unsigned type:4;//物件型別(4位=0.5位元組)
    unsigned encoding:4;//編碼(4位=0.5位元組)
    unsigned lru:LRU_BITS;//記錄物件最后一次被應用程式訪問的時間(24位=3位元組)
    int refcount;//參考計數,等于0時表示可以被垃圾回收(32位=4位元組)
    void *ptr;//指向底層實際的資料存盤結構,如:sds等(8位元組)
} robj;

當我們在 Redis 客戶端中執行命令 set name lonely_wolf ,就會得到下圖所示的一個結構(省略了部分屬性):
在這里插入圖片描述

看到這個圖想必大家會有疑問,這里面的 typeencoding 到底是什么呢?其實這兩個屬性非常關鍵,Redis 就是通過這兩個屬性來識別當前的 value 到除錯于哪一種基本資料型別,以及當前資料型別的底層采用了何種資料結構進行存盤,

type 屬性

type 屬性表示物件型別,其對應了 Redis 當中的 5 種基本資料型別:

型別屬性描述type命令回傳值
REDIS_STRING字串物件string
REDIS_LIST串列物件list
REDIS_HASH哈希物件hash
REDIS_SET集合物件set
REDIS_ZSET有序集合物件zset

可以看到,這就是對應了我們 5 種常用的基本資料型別,

encoding 屬性

Redis 當中每種資料型別都是經過特別設計的,相信大家看完這個系列也會體會到 Redis 設計的精妙之處,字串在我們眼里是非常簡單的一種資料結構了,但是 Redis 卻把它優化到了極致,為了節省空間,其通過編碼的方式定義了三種不同的存盤方式:

編碼屬性描述object encoding命令回傳值
OBJ_ENCODING_INT使用整數的字串物件int
OBJ_ENCODING_EMBSTR使用 embstr 編碼實作的字串物件embstr
OBJ_ENCODING_RAW使用 raw 編碼實作的字串物件raw
  • int 編碼
    當我們用字串物件存盤的是整型,且能用 8 個位元組的 long 型別進行表示(即 263 次方減 1),則 Redis 會選擇使用 int 編碼來存盤,此時 redisObject 物件中的 ptr 指標直接替換為 long 型別,我們想想 8 個位元組如果用字串來存盤只能存 8 位,也就是千萬級別的數字,遠遠達不到 263 次方減 1 這個級別,所以如果都是數字,用 long 型別會更節省空間,
  • embstr 編碼
    當字串物件中存盤的是字串,且長度小于 44Redis 3.2 版本之前是 39)時,Redis 會選擇使用 embstr 編碼來存盤,
  • raw 編碼
    當字串物件中存盤的是字串,且長度大于 44 時,Redis 會選擇使用 raw 編碼來存盤,

講了半天理論,接下來讓我們一起來驗證下這些結論,依次輸入 set name lonely_wolftype nameobject encoding name 命令:
在這里插入圖片描述

可以發現當前的資料型別就是 string,普通字串因為長度小于 44,所以采用的是 embstr 編碼,

再依次輸入:set num 1111111111set address aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(長度 44),set address aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(長度 45),分別查看型別和編碼:

在這里插入圖片描述
可以發現,當輸入純數字的時候,采用的是 int 編碼,而字串小于等于 44 則為 embstr,大于 44 則為 raw 編碼,

字串物件中除了上面提到的純整數和字串,還可以存盤浮點型型別,所以字串物件可以存盤以下三種型別:

  • 字串
  • 整數
  • 浮點數

而當我們的 value 為整數時,還可以使用原子自增命令來實作 value 的自增,這個命令在實際開發程序中非常實用,

  • incr:自增 1
  • incrby:自增指定數值,
    在這里插入圖片描述

不過這兩個命令只能用在 value 為整數的場景,當 value 不是整數時則會報錯,

embstr 編碼為什么從 39 位修改為 44 位

embstr 編碼中,redisObjectsds 是連續的一塊記憶體空間,這塊記憶體空間 Redis 限制為了 64 個位元組,而redisObject 固定占了16位元組(上面定義中有標注),Redis 3.2 版本之前的 sds 占了 8 個位元組,再加上字串末尾 \0 占用了 1 個位元組,所以:64-16-8-1=39 位元組,

Redis 3.2 版本之后 sds 做了優化,對于 embstr 編碼會采用 sdshdr8 來存盤,而 sdshdr8 占用的空間只有 24 位:3 位元組(len+alloc+flag)+ \0 字符(1位元組),所以最后就剩下了:64-16-3-1=44 位元組,

embstr 編碼和 raw 編碼的區別

embstr 編碼是一種優化的存盤方式,其在申請空間的時候因為 redisObjectsds 兩個物件是一個連續空間,所以只需要申請 1 次空間(同樣的,釋放記憶體也只需要 1 次),而 raw 編碼因為 redisObjectsds 兩個物件的空間是不連續的,所以使用的時候需要申請 2 次空間(同樣的,釋放記憶體也需要 2 次),但是使用 embstr 編碼時,假如需要修改字串,那么因為 redisObjectsds 是在一起的,所以兩個物件都需要重新申請空間,為了避免這種情況發生,embstr 編碼的字串是只讀的,不允許修改
在這里插入圖片描述

上圖中的示例我們看到,對一個 embstr 編碼的字串物件進行 append 操作時,長度還沒有達到 45,但是編碼已經被修改為 raw 了,這就是因為 embstr 編碼是只讀的,如果需要對其修改,Redis 內部會將其修改為 raw 編碼之后再操作,同樣的,如果是操作 int 編碼的字串之后,導致 long 型別無法存盤時(int 型別不再是整數或者長度超過 263 次方減 1 時),也會將 int 編碼修改為 raw 編碼,

PS:需要注意的是,編碼一旦升級(int–>embstr–>raw),即使后期再把字串修改為符合原編碼能存盤的格式時,編碼也不會回退,

總結

本文主要講述了 Redis 當中最常用的字符創物件,通過二進制安全字串的特別逐步分析了 sds 的底層存盤即編碼格式,并分別介紹了每種編碼格式的區別,最后通過示例來演示了編碼的轉換程序,

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/247693.html

標籤:其他

上一篇:學習分享:POI-TL 匯出Word復雜表格合并分享

下一篇:HTML5知識點總結(三)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more