LightKV-高性能key-value存儲組件

注:此組件不夠成熟洪己,筆者最近寫了一個更好的KV存儲組件:
https://juejin.cn/post/7018522454171582500

LightKV是基于Java NIO的輕量級妥凳,高性能,高可靠的key-value存儲組件答捕。

一逝钥、起源

Android平臺常見的本地存儲方式, SDK內置的有SQLite,SharedPreference等拱镐,開源組件有ACache, DiskLruCahce等艘款,有各自的特點和適用性。
SharedPreference以其天然的 key-value API沃琅,二級存儲(內存HashMap, 磁盤xml文件)等特點哗咆,為廣大開發(fā)者所青睞。
然而益眉,任何工具都是有適用性的晌柬,參見文章《不要濫用SharedPreference》。
當然郭脂,其中一些缺點是其定位決定的年碘,比如說不適合存儲大的key-value, 這個無可厚非;
不過有一些地方可以改進展鸡,比如存儲格式:xml解析速度慢屿衅,空間占用大,特殊字符需要轉義等特點莹弊,對于高頻變化的存儲涤久,實非良策。
故此忍弛,有必要寫一個改良版的key-value存儲組件响迂。

二、LightKV原理

2.1 存儲格式

我們希望文件可以流式解析细疚,對于簡單key-value形式栓拜,完全可以自定義格式。
例如惠昔,簡單地依次保存key-value就好:
key|value|key|value|key|value……

value

關于value類型幕与,我們需要支持一些常用的基礎類型:boolean, int, long, float, double, 以及String 和 數(shù)組(byte[])。
尤其是后者镇防,更多的復合類型(比如對象)都可以通過String和數(shù)組轉化啦鸣。
作為底層的組件,支持最基本的類型可以簡化復雜度来氧。
對于String和byte[], 存儲時先存長度诫给,再存內容香拉。

key

我們觀察到,在實際使用中key通常是預先定義好的中狂;
故此凫碌,我們可以舍棄一定的通用性,用int來作為key, 而非用String胃榕。
有舍必有得盛险,用int作為key,可以用更少的空間承載更多的信息勋又。

public interface DataType {
    int OFFSET = 16;
    int MASK = 0xF0000;
    int ENCODE = 1 << 20;

    int BOOLEAN = 1 << OFFSET;
    int INT = 2 << OFFSET;
    int FLOAT = 3 << OFFSET;
    int LONG = 4 << OFFSET;
    int DOUBLE = 5 << OFFSET;
    int STRING = 6 << OFFSET;
    int ARRAY = 7 << OFFSET;
}

int的低16位用來定義key,
17-19位用來定義類型苦掘,
20位預留,
21位標記是否編碼(后面會講到)楔壤,
32位(最高位)標記是否有效:為1時為無效鹤啡,讀取時會跳過。

內存緩存

SharePreference相對于ACache蹲嚣,DiskLruCache等多了一層內存的存儲递瑰,于是他們的定位也就涇渭分明了:
后者通常用于存儲大對象或者文件等,他們只負責提供磁盤存儲,至于讀到內存之后如果使用和管理,則不是他們的職責了矗漾。
太大的對象會占用太多的內存,而SharePreference是長期持有引用您朽,沒有空間限制和淘汰機制的狂丝,因此SharePreference適用于“輕量級存儲”换淆, 而由此所帶來的收益就是讀取速度很快。
LightKV定位也是“輕量級存儲”几颜,所以也會在內存中存儲key-value倍试,只不過這里用SparseArray來存儲。

2.2 存儲操作

上面提到, 存儲格式是簡單地key-value依次排列:
key|value|key|value|key|value……
這樣存放蛋哭,讀取時可以流式地解析县习,甚至,寫入時可以增量寫入谆趾。

方案一躁愿、增量&異步

增量操作
  • 新增:在尾部追加key|value即可;
  • 刪除:為了避免字節(jié)移動沪蓬,可以用標記的方法——將key的最高位標記為1彤钟;
  • 修改:如果value長度不變,尋址到對應的位置跷叉,寫入value即可逸雹;否則营搅,先“刪除”,再“新增”梆砸;
  • GC: 解析文件內容時(加載數(shù)據時進行解析)转质,記錄被刪除的內容的長度,大于設定閾值則清空文件帖世,做一次全量寫入休蟹。 使用過程中,有“刪除”操作時狮暑,“刪除”之后鸡挠,累加刪除的內容的長度,若超過設定閾值則GC搬男。
mmap

要想增量修改文件拣展,需要具備隨機寫入的能力:
Java NIO會是不錯的選擇,甚至缔逛,可以用mmap(內存映射文件)备埃。
mmap還有一些優(yōu)點:
1、直接操作內核空間:避免內核空間和用戶空間之間的數(shù)據拷貝褐奴;
2按脚、自動定時刷新:避免頻繁的磁盤操作;
3敦冬、進程退出時刷新:系統(tǒng)層面的調用辅搬,不用擔心進程退出導致數(shù)據丟失。

如果要說不足脖旱,就是在映射文件階段比常規(guī)的IO的打開文件消耗更多堪遂。
所以API中建議大文件時采用mmap,小文件的讀寫用建議用常規(guī)IO萌庆;而網上介紹mmap也多是舉例大文件的拷貝溶褪。
事實上如果小文件是高頻寫入的話,也是值得一試的践险,
比如騰訊的日志組件 xlog 和 存儲組件 MMKV, 都用了mmap猿妈。

mmap的寫入方式其實類似于異步寫入,只是不需要自己開線程去刷數(shù)據到磁盤巍虫,而是由操作系統(tǒng)去調度彭则。
這樣的方式有利有弊:好處是寫入快,減少磁盤損耗占遥;
缺點就是俯抖,和SharePreference的apply一樣,不具備原子性筷频,沒有入原子性蚌成,一致性就得不到保障前痘。
比如,數(shù)據寫入內存后担忧,在數(shù)據刷新到磁盤之前芹缔,發(fā)生系統(tǒng)級錯誤(如系統(tǒng)崩潰)或設備異常(如斷電,磁盤損壞等)瓶盛,此時會丟失數(shù)據最欠;
如果寫入內存后,刷入磁盤前惩猫,有別的代碼讀取了剛才寫入的內存芝硬,就有可能導致數(shù)據不一致。

不過轧房,通常情況下拌阴,發(fā)生系統(tǒng)級錯誤和設備異常的概率較低,所以還是比較可靠的奶镶。

方案二迟赃、全量&同步

對于一些核心數(shù)據,我們希望用更可靠的方式存儲厂镇。
怎么定義可靠呢纤壁?
首先原子性是要有的,所以只能同步寫入了捺信;
然后是可用性和完整性:
程序異常酌媒,系統(tǒng)異常,或者硬件故障等都可能導致數(shù)據丟失或者錯誤迄靠;
需添加一些機制確保異常和故障發(fā)生時數(shù)據仍然完整可用秒咨。

查看SharedPreference源碼,其容錯策略是梨水,
寫入前重命名主文件為備份文件的名字拭荤,成功寫入則刪除備份文件茵臭,
而打開文件階段疫诽,如果發(fā)現(xiàn)有備份文件,將備份文件重命名為主文件的名字旦委。
從而奇徒,假如寫入數(shù)據時發(fā)生故障,再次重啟APP時可以從備份文件中恢復數(shù)據缨硝。
這樣的容錯策略摩钙,總體來說是不錯的方案,能保證大多數(shù)據情況下的數(shù)據可用性查辩。
我們沒有采用該方案胖笛,主要是考慮該方案操作相對復雜网持,以及其他一些顧慮。

我們采用的策略是:冗余備份+數(shù)據校驗长踊。

冗余備份

冗余備份來提高數(shù)據數(shù)據可用性的思想在很多地方有體現(xiàn)功舀,比如 RAID 1 磁盤陣列。
同樣身弊,我們可以通過一份內存寫兩個文件辟汰,這樣當一個文件失效,還有另外一個文件可用阱佛。
比方說一個文件失效的概率時十萬分之一帖汞,則兩個文件同時失效的概率是百億分之一。
總之凑术,冗余備份可以大大減少數(shù)據丟失的概率翩蘸。
有得必有失,其代價就是雙倍磁盤空間和寫入時間淮逊。

不過我們的定位是“輕量級存儲”鹿鳖,如果只存“核心數(shù)據”,數(shù)據量不會很大壮莹,所以總的來說收益大于代價翅帜。
就寫入時間方面,相比SharedPreference而言命满,重命名和刪除文件也是一種IO涝滴,其本質是更新文件的“元數(shù)據”。
寫磁盤以頁(page)為單位胶台,一頁通常為4K歼疮。

向文件寫入1個字節(jié)和2497字節(jié),在磁盤寫入階段是等價的(都需要占用4K的字節(jié))诈唬。
數(shù)據量較少時韩脏,寫入兩份文件,相比于“重命名->寫數(shù)據->刪除文件”的操作铸磅,區(qū)別不大赡矢。

數(shù)據校驗

數(shù)據校驗的方法通常是對數(shù)據進行一些的運算,將運算結果放在數(shù)據后阅仔;讀取時做同樣運算吹散,然后和之前的結果對比。
常見的方法有奇偶校驗八酒,CRC, MD5, SHA等空民。
奇偶校驗多被應用于計算機硬件的錯誤檢測中; 軟件層面,通常是計算散列羞迷。
眾多Hash算法中界轩,我們選擇 64bit 的 MurmurHash, 關于MurmurHash可查看筆者的另一篇文章《漫談散列函數(shù)》画饥。

在考慮分組寫入還全量寫入,分組校驗還是全量校驗時浊猾,
分組的話荒澡,細節(jié)多,代碼復雜与殃,還是選擇全量的方式吧单山。
也就是,收集所有key|value到buffer, 然后計算hash, 放到數(shù)據后幅疼,一并寫入次磁盤米奸。

魚和熊掌

不同的應用場景有不同的需求。
LightKV同時提供了快速寫入的mmap方式爽篷,和更可靠寫入的同步寫入方式悴晰。
它們有相同的API,只是存儲機制不一樣逐工。

public abstract class LightKV {
    final SparseArray<Object> mData = new SparseArray<>();
    //......
}

public class AsyncKV extends LightKV {
    private FileChannel mChannel;
    private MappedByteBuffer mBuffer;
    //......
}

public class SyncKV extends LightKV {
    private FileChannel mAChannel;
    private FileChannel mBChannel;
    private ByteBuffer mBuffer;
    //......
}

AsyncKV由于不具備一致性铡溪,所以也沒有必要冗余備份了,寫一份就好泪喊,以求更高的寫入效率和更少磁盤寫入棕硫。
SyncKV由于要做冗余備份,所以需要打開兩個文件袒啼,而buffer用同一份即可哈扮;
兩者的特點在前面“方案一”和“方案二”中有所闡述了,根據具體需求靈活使用即可蚓再。

2.3 混淆操作

對于用XML來存儲的SharePreferences來說滑肉,打開其文件即可一覽所有key-value, 即使開發(fā)者對value進行編碼,key還是可以看到的摘仅。
SharePreferences的文件不是存在App下的目錄靶庙,在沙盒之中嗎?
無root權限下娃属,對于其他應用(非系統(tǒng))六荒,沙盒確實是不可訪問的;
但是對于APP逆向者(黑色產業(yè)膳犹?)來說恬吕,SharePreferences文件不過是囊中之物签则,或可從中一窺APP的關鍵须床,以助其破解APP。
故此渐裂,混淆內容文件豺旬,或可增加一點破解成本钠惩。
對于APP來說,沒有絕對的安全族阅,只是破解成本與收益之間的博弈篓跛,這里就不多作展開了。

LightKV由于采用流式存儲坦刀,而且key是用int類型愧沟,所以不容易看出其文件內容;
但是如果value是明文字符串鲤遥,還是可以看到部分內容的沐寺,如下圖:


LightKV提供了混淆value(String和byte[]類型)的接口:

    public interface Encoder {
        byte[] encode(byte[] src);
        byte[] decode(byte[] des);
    }

開發(fā)者可以按照自己的規(guī)則實現(xiàn)編碼和解碼。
通過該接口可以做很多擴展:

  • 1盖奈、嚴格的加密混坞;
  • 2、數(shù)據壓縮钢坦;
  • 3究孕、內容混淆(事實上前二者都有混淆的功能)

混淆后,打開文件爹凹,都是亂碼厨诸。

值得一提的是,只能對String和byte[]類型的value混淆禾酱。
因為基礎類如long, double等,以二進制形式寫入,用文本的形式打開,本就是不好閱讀的,無需再作混淆泳猬。

三、使用方法

前面我們看到宇植,SyncKV和AsyncKV都繼承于LightKV, 二者在內存中的存儲格式是一致的得封,都是SparseArray,
所以get方法封裝在LightKV中,然后各自實現(xiàn)put方法指郁。
方法列表如下圖:


和SharePreferences類似忙上,也有contains, remove, clear 和 commit 方法,甚至于闲坎,具體用法也很類似:

public class AppData {
    private static final SharedPreferences sp = 
        GlobalConfig.getAppContext().getSharedPreferences("app_data", Context.MODE_PRIVATE);
    
    private static final SharedPreferences.Editor editor = sp.edit();

    private static final String ACCOUNT = "account";
    private static final String TOKEN = "token";

    private static void putString(String key, String value) {
        editor.putString(key, value);
        editor.commit();
    }

    private static String getString(String key) {
        return sp.getString(key, "");
    }
}
public class AppData {
    private static final SyncKV DATA =
            new LightKV.Builder(GlobalConfig.getAppContext(), "app_data")
                    .logger(AppLogger.getInstance())
                    .executor(AsyncTask.THREAD_POOL_EXECUTOR)
                    .encoder(new ConfuseEncoder())
                    .sync();

    public interface Keys {
        int SHOW_COUNT = 1 | DataType.INT;
        int ACCOUNT = 2 | DataType.STRING | DataType.ENCODE;
        int TOKEN = 3 | DataType.STRING | DataType.ENCODE;
    }

    public static SyncKV data() {
        return DATA;
    }

    public static String getString(int key) {
        return DATA.getString(key);
    }

    public static void putString(int key, String value) {
        DATA.putString(key, value);
        DATA.commit();
    }
}

當然疫粥,以上只是眾多封裝方法中的一種,具體使用中腰懂,不同的開發(fā)者有不同的偏好梗逮。

對于LightKV而言,key的定義方法如下:
1绣溜、最好一個文件對應一個統(tǒng)一定義key的類慷彤,如上面的“Keys”;
2、key的賦值底哗,按類型從1到65534都可以定義岁诉,然后和對應的DataType做“|”運算(解析的時候需要據此判斷類型)。

相對于SharePreferences跋选,LightKV有更多的初始化選項涕癣,故而用構造者模式來構建對象。
下面逐一說明各個參數(shù)和對應的特性前标。

3.1 內容混淆

若需要對value混淆坠韩,只需在構造LightKV時傳入Encoder,
然后聲明key時和DataType.ENCODE做“|”運算即可。
保存和讀取時炼列,LightKV會將key和DataType.ENCODE做“&”運算同眯,若不為0,則調用Encoder進行編碼(保存)或解碼(讀任ㄑ肌)须蜗。

3.2 異步加載

SharePreferences的加載在新創(chuàng)建的的線程中加載的, 在完成加載之前阻塞讀和寫:
LightKV同樣實現(xiàn)了異步加載, 而且可以指定 Executor,當然也可以選擇不異步加載(不傳Executor即可)目溉。
需要提醒的是明肮,雖然提供了異步加載,但是有時候沒有異步加載的效果缭付。
比如對象初始化的同時立即調用get或者put方法柿估,會阻塞當前線程直到加載完成,這樣和同步加載沒什么區(qū)別陷猫。

建議寫法秫舌,在進程初始化的時候調用data(), 以觸發(fā)數(shù)據的加載:

    fun init(context: Context) {
        // 僅初始化對象,不做get和put
        AppData.data()

        // 其他初始化工作
    }

3.3 錯誤日志

    public interface Logger {
        void e(String tag, Throwable e);
    }

大多數(shù)組件都不能保證運行期不發(fā)生異常绣檬,發(fā)生異常時足陨,開發(fā)者通常會把異常信息打印到日志文件(有的還會上傳云端)。
故此娇未,LightKV提供了打印日志接口墨缘,傳入實現(xiàn)類即可。

3.4 選擇模式

在Builder的最后零抬,調用 sync() 和 async() 可分辨創(chuàng)建AsyncKV和SyncKV镊讼。
各自的特點前面也交代過了,靈活選取即可平夜。
如果不是存一些十分重要的數(shù)據(比如帳號信息等)蝶棋,用AsyncKV即可。

3.5 訪問數(shù)據

寫完初始化參數(shù)忽妒,定義好key, 編寫 get 和 set方法之后,
就可以訪問數(shù)據了:

String account = AppData.getString(AppData.Keys.ACCOUNT)
if(TextUtils.isEmpty(account)){
      AppData.putString(AppData.Keys.ACCOUNT, "foo@gmail.com")
}

3.6 Kotlin下的用法

借助Kotlin的委托屬性玩裙,筆者拓展了LightKV的API, 提供了更方便的用法兼贸。

object AppData : KVData() {
    override val data: LightKV by lazy {
        LightKV.Builder(GlobalConfig.appContext, "app_data")
                .logger(AppLogger)
                .executor(AsyncTask.THREAD_POOL_EXECUTOR)
                .encoder(GzipEncoder)
                .async()
    }

    var showCount by int(1)
    var account by string(2)
    var token by string(3)
    var secret by array(4 or DataType.ENCODE)
}
val account = AppData.account
if (TextUtils.isEmpty(account)) {
   AppData.account = "foo@gmail.com"
}

與Java版的API相比,key的聲明更加簡單献酗,而且可以像訪問變量一樣訪問key對應的value寝受。

四坷牛、評測

倉促之間罕偎,準備的測試用例可能不是很科學,僅供參考-_-

測試用例中京闰,對支持的7種類型各配置5個key, 共35對key|value颜及。

4.1 存儲空間

存儲方式 文件大小(kb)
AsyncKV 4
SyncKV 1.7
SharePreferences 3.3

AsyncKV由于采用mmap的打開方式,需要映射一塊磁盤空間到內存蹂楣,為了減少碎片俏站,故而一次映射一頁(4K)。
SyncKV由于存儲格式比較緊湊痊土,所以文件大小相比SharePreferences要幸拊;
但是由于SyncKV采用雙備份赁酝,所以總大小和SharePreferences差不多犯祠。

數(shù)據量都少于4K時,其實三者相差無幾酌呆;
當存儲內容變多時衡载,AsyncKV反而會更少占用,因為其存儲格式和SyncKV一樣隙袁,但是只用存一份痰娱。

4.2 寫入性能

理想中的寫入是各組key|value全寫到內存,然后統(tǒng)一調用一次commit, 這樣寫入是最快的菩收。
然而實際使用中梨睁,各組key|value的寫入通常是隨機的,所以下面測試結果娜饵,都是每次put后立即提交而姐。
AsyncKV例外,因為其定位就是減少IO划咐,讓系統(tǒng)內核自己去提交更新拴念。

測試機器1:小米 note 1 (2018年5月)

存儲方式 寫入耗時(毫秒)
AsyncKV 2.25
SyncKV 75.34
SharePreferences-apply 6.90
SharePreferences-commit 279.14

測試機器2:華為P30 pro (2020年3月)

存儲方式 寫入耗時(毫秒)
AsyncKV 0.31
SyncKV 8.31
SharePreferences-apply 1.9
SharePreferences-commit 30.81

(新機器寫入速度確實快很多-_-)

AsyncKV 和 SharePreferences-apply 這兩種方式,提交到內存后立即返回褐缠,所以耗時較少政鼠;
SyncKV 和 SharePreferences-commit,都是在當前線程提交內存和磁盤队魏,故而耗時較長公般。
無論是同步寫入還是異步寫入万搔,LightKV都要比SharePreferences快:
在同步寫入方面,SharePreferences-commit耗時比SyncKV多3到4倍官帘;
在異步寫入方面瞬雹,AsyncKV也比SharePreferences-apply也要快很多。

至于加載性能刽虹,筆者比較了華為P30pro和小米Note1的機器酗捌,發(fā)現(xiàn)AsyncKV的loading時間在小米note上相對較慢,而在華為P30pro上則相對較快涌哲,所以就不貼出數(shù)據了胖缤。
文末有github鏈接,讀者可自行run一下benchmark阀圾。

然后就是讀取性能哪廓,SharePreferences是從HashMap中讀取,LightKV是從SparseArray中讀取初烘,兩種數(shù)據結構的優(yōu)點和缺點網上已經有很多討論了涡真,這次就不多作比較了。

五肾筐、總結

SharePreferences是Android平臺輕量且方便的key-value存儲組件哆料,然而不少可以改進的地方。
LightKV以SharePreferences為參考局齿,從效率剧劝,安全和易用性等方面,提供更好的存儲方式抓歼。

六讥此、下載

dependencies {
    implementation 'com.horizon.lightkv:lightkv:1.0.7'
}

項目地址:
https://github.com/BillyWei001/LightKV

參考文章:
http://www.cnblogs.com/mingfeng002/p/5970221.html
https://cloud.tencent.com/developer/article/1066229
https://segmentfault.com/r/1250000007474916?shareId=1210000007474917
http://www.reibang.com/p/ad9756fe21c8
http://www.reibang.com/p/07664dc4c51a

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市谣妻,隨后出現(xiàn)的幾起案子萄喳,更是在濱河造成了極大的恐慌,老刑警劉巖蹋半,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件他巨,死亡現(xiàn)場離奇詭異,居然都是意外死亡减江,警方通過查閱死者的電腦和手機染突,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來辈灼,“玉大人份企,你說我怎么就攤上這事⊙灿ǎ” “怎么了司志?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵甜紫,是天一觀的道長。 經常有香客問我骂远,道長囚霸,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任激才,我火速辦了婚禮拓型,結果婚禮上,老公的妹妹穿的比我還像新娘贸营。我一直安慰自己吨述,他們只是感情好岩睁,可當我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布钞脂。 她就那樣靜靜地躺著,像睡著了一般捕儒。 火紅的嫁衣襯著肌膚如雪冰啃。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天刘莹,我揣著相機與錄音阎毅,去河邊找鬼。 笑死点弯,一個胖子當著我的面吹牛扇调,可吹牛的內容都是我干的。 我是一名探鬼主播抢肛,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼狼钮,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了捡絮?” 一聲冷哼從身側響起熬芜,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎福稳,沒想到半個月后涎拉,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡的圆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年鼓拧,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片越妈。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡季俩,死狀恐怖,靈堂內的尸體忽然破棺而出叮称,到底是詐尸還是另有隱情种玛,我是刑警寧澤藐鹤,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站赂韵,受9級特大地震影響娱节,放射性物質發(fā)生泄漏。R本人自食惡果不足惜祭示,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一肄满、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧质涛,春花似錦稠歉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至毡代,卻和暖如春阅羹,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背教寂。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工捏鱼, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人酪耕。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓导梆,卻偏偏與公主長得像,于是被迫代替她去往敵國和親迂烁。 傳聞我的和親對象是個殘疾皇子看尼,可洞房花燭夜當晚...
    茶點故事閱讀 43,465評論 2 348

推薦閱讀更多精彩內容

  • 一、MySQL架構與歷史 A.并發(fā)控制 1.共享鎖(shared lock婚被,讀鎖):共享的狡忙,相互不阻塞的 2.排他...
    ZyBlog閱讀 19,827評論 3 177
  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn)址芯,斷路器灾茁,智...
    卡卡羅2017閱讀 134,628評論 18 139
  • feisky云計算、虛擬化與Linux技術筆記posts - 1014, comments - 298, trac...
    不排版閱讀 3,827評論 0 5
  • 曾經有段話是這樣說的:女生努力賺錢的意義就是碰到自己心怡的男生時谷炸,可以說我只要愛情北专,面包我自己有。我們之所以努力旬陡,...
    鋒雲星璇閱讀 489評論 2 1
  • 高溫的季節(jié)即將到來拓颓,雞皮膚的你難道還要像往年那樣蒸桑拿般的度過夏天嗎?你以為你長袖長褲把雞皮皮膚遮起來就沒事了嗎描孟?...
    你的昵稱啊3閱讀 354評論 0 0