一次重大的線上事故的總結

問題背景描述

服務以及上線幾個月,今天在客服查詢用戶的提現信息的時候,發現有的用戶竟然出現了高達數千數萬的提現請求.由于我們的用戶體量并沒有那么大,交易流水按理說也不應該有那么多,客服帶著疑問跟我們報出了這個問題.
于是,我便查詢了最近的交易記錄,發現有幾個人是定向的而且多次的大額交易記錄,然后我便查詢了一下充值記錄,發現用戶的充值記錄是根本沒有那么多的,也就是說用戶的賬戶余額發生了異常,那么問題出在哪了呢?

發現問題

帶著上面的疑問,我仔細的檢查了交易記錄的數據,發現在交易記錄中,有針對某個功能同一時間的大量請求,然后第一時間去查詢用戶的訪問日志,發現存在某一ip同一時間內下針對某個接口地址的大量請求.

例如:

102.1.1.X 2016-10-23 16:66:01 /xxxxxxx/v1/money/opt/pee/ Post
102.1.1.X 2016-10-23 16:66:01 /xxxxxxx/v1/money/opt/pee/ Post
102.1.1.X 2016-10-23 16:66:01 /xxxxxxx/v1/money/opt/pee/ Post
102.1.1.X 2016-10-23 16:66:01 /xxxxxxx/v1/money/opt/pee/ Post
102.1.1.X 2016-10-23 16:66:01 /xxxxxxx/v1/money/opt/pee/ Post
102.1.1.X 2016-10-23 16:66:01 /xxxxxxx/v1/money/opt/pee/ Post
102.1.1.X 2016-10-23 16:66:01 /xxxxxxx/v1/money/opt/pee/ Post

由此猜測,是被用戶用程序盜刷了接口.
那么追究其本質問題,就算是用戶盜刷了接口,也不應該出現余額異常的問題,對應著這個接口的代碼順藤摸瓜屢下去.

原代碼是這個樣子的(由于涉及到具體業務,這里用偽代碼來代替):

    // 參數校驗
    if(StringUtils.isEmty(xx)){
       throw new Exception(Error.param_error);
    } 
    // 用戶余額校驗 
    1.余額是否大于0
    2.余額是否充足
    // 檢測是否查看過  
    boolean b = seeLogService.hasSee();
    if(b) throw new Exception(Error.has_see);
    // 更新查看日志
    seeLogService.update(xx);
    // 增加收入者用戶余額并記錄日志
    OrdersService.addUserGoldNum
    // 減少消費者用戶余額并記錄日志
    OrdersService.subtractUserGoldNum

讓我們仔細看一看上面的代碼,會有什么問題?
這里我們先帶著我們想到的問題之處,去測試一下,首先我用CyclicBarrier做了一個并發的請求工具,工具類的代碼如下:


import com.alibaba.fastjson.JSON;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Executors+CyclicBarrier實現的并發測試小例子<br>
 * 例子實現了并發測試中使用的集合點,集合點超時時間及思考時間等技術
 *
 * @author 王光東
 * @version 1.0
 * @date 2016年06月27日16:43:49
 */
public class FlushGeneratorPost {
    // 主線程停止的標志
    private static volatile boolean runFlag = true;
    // 欄桿
    private CyclicBarrier cyclicBarrier;
    // 線程數
    private static int threads;
    // 總數
    private static AtomicInteger totalCount = new AtomicInteger();
    // 已經開啟的線程數
    private static AtomicInteger startedCount = new AtomicInteger();
    // 已經完成任務的線程數
    private static AtomicInteger finishCount = new AtomicInteger();
    // 正在進行的線程數
    private static AtomicInteger runCount = new AtomicInteger();
    // 成功的線程數
    private static AtomicInteger successCount = new AtomicInteger();
    // 失敗的線程數
    private static AtomicInteger failCount = new AtomicInteger();
    // 請求的url地址
    private String url;
    // 集合點超時時間
    private long rendzvousWaitTime = 0;
    // 思考時間
    private long thinkTime = 0;
    // 次數
    private static int iteration = 0;

    /**
     * 初始值設置
     *
     * @param url               被測url
     * @param threads           總線程數
     * @param iteration         每個線程迭代次數
     * @param rendzvousWaitTime 集合點超時時間,如果不啟用超時時間,請將此值設置為0.<br>
     *                          如果不啟用集合點,請將此值設置為-1<br>
     *                          如果不啟用超時時間,則等待所有的線程全部到達后,才會繼續往下執行<br>
     * @param thinkTime         思考時間,如果啟用思考時間,請將此值設置為0
     */
    public FlushGeneratorPost(String url, int threads, int iteration, long rendzvousWaitTime,
        long thinkTime) {
        totalCount.getAndSet(threads);
        FlushGeneratorPost.threads = threads;
        this.url = url;
        this.iteration = iteration;
        this.rendzvousWaitTime = rendzvousWaitTime;
        this.thinkTime = thinkTime;
    }

    // 過得線程數的信息
    public static ThreadCount getThreadCount() {
        return new ThreadCount(threads, runCount.get(), startedCount.get(), finishCount.get(),
            successCount.get(), failCount.get());
    }

    // 判斷線程是否應該停止
    public static boolean isRun() {
        return finishCount.get() != threads;
    }

    // 優雅的停止線程
    public synchronized static void stop() {
        runFlag = false;
    }

    // 執行任務
    public void runTask() {
        List<Future<String>> resultList = new ArrayList<Future<String>>();
        // 線程池構造
        ExecutorService exeService = Executors.newFixedThreadPool(threads);
        cyclicBarrier = new CyclicBarrier(threads);//默認加載全部線程
        for (int i = 0; i < threads; i++) {
            resultList.add(
                exeService.submit(new TaskThread(i, url, iteration, rendzvousWaitTime, thinkTime)));
        }
        exeService.shutdown();
        for (int j = 0; j < resultList.size(); j++) {
            try {
                System.out.println(resultList.get(j).get());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        stop();
    }

    /**
     * 不同狀態的線程數構造類
     */
    static class ThreadCount {
        public final int runThreads;
        public final int startedThreads;
        public final int finishedThreads;
        public final int totalThreads;
        public final int successCount;
        public final int failCount;

        public ThreadCount(int totalThreads, int runThreads, int startedThreads,
            int finishedThreads, int successCount, int failCount) {
            this.totalThreads = totalThreads;
            this.runThreads = runThreads;
            this.startedThreads = startedThreads;
            this.finishedThreads = finishedThreads;
            this.successCount = successCount;
            this.failCount = failCount;
        }
    }

    /**
     * 實際的業務線程類
     */
    private class TaskThread implements Callable<String> {
        private String url;
        private long rendzvousWaitTime = 0;
        private long thinkTime = 0;
        private int iteration = 0;
        private int iterCount = 0;
        private int taskId;

        /**
         * 任務執行者屬性設置
         *
         * @param taskId            任務id號
         * @param url               被測url
         * @param iteration         迭代次數,如果一直執行則需將此值設置為0
         * @param rendzvousWaitTime 集合點超時時間,如果不需要設置時間,則將此值設置為0。如果不需要設置集合點,則將此值設置為-1
         * @param thinkTime         思考時間,如果不需要設置思考時間,則將此值設置為0
         */
        public TaskThread(int taskId, String url, int iteration, long rendzvousWaitTime,
            long thinkTime) {
            this.taskId = taskId;
            this.url = url;
            this.rendzvousWaitTime = rendzvousWaitTime;
            this.thinkTime = thinkTime;
            this.iteration = iteration;
        }

        @Override
        public String call() throws Exception {
            startedCount.getAndIncrement();
            runCount.getAndIncrement();
            while (runFlag && iterCount < iteration) {
                if (iteration != 0)
                    iterCount++;
                try {
                    if (rendzvousWaitTime > 0) {
                        try {
                            System.out.println("任務:task-" + taskId + " 已到達集合點...等待其他線程,集合點等待超時時間為:"
                                + rendzvousWaitTime);
                            cyclicBarrier.await(rendzvousWaitTime, TimeUnit.MICROSECONDS);
                        } catch (InterruptedException e) {
                        } catch (BrokenBarrierException e) {
                            System.out.println(
                                "task-" + taskId + " 等待時間已超過集合點超時時間:" + rendzvousWaitTime
                                    + " ms,將開始執行任務....");
                        } catch (TimeoutException e) {
                        }
                    } else if (rendzvousWaitTime == 0) {
                        try {
                            System.out.println("任務:task-" + taskId + " 已到達集合點...等待其他線程");
                            cyclicBarrier.await();
                        } catch (InterruptedException e) {
                        } catch (BrokenBarrierException e) {
                        }
                    }
                    // 發送請求返回結果
                    Bean result = readContent(url);
                    System.out.println(
                        "線程:task-" + taskId + " 獲取到的資源大小:" + result.getResult().length() + ",狀態碼:"
                            + result.getState());
                    // 增加成功的值
                    successCount.getAndIncrement();
                    // 判斷是否需要思考
                    if (thinkTime != 0) {
                        System.out.println("task-" + taskId + " 距下次啟動時間:" + thinkTime);
                        Thread.sleep(thinkTime);
                    }
                } catch (Exception e) {
                    failCount.getAndIncrement();
                }
            }
            // 增加完成次數
            finishCount.getAndIncrement();
            // 減少運行的線程數量
            runCount.decrementAndGet();
            return Thread.currentThread().getName() + " 執行完成!";
        }
    }

    public static void main(String[] args) {
        final long startTime = System.currentTimeMillis();
        String baseUri = "http://localhost:8080/xxx/xx/xx/xx/xx";
        new Thread() {
            public void run() {   
                new FlushGeneratorPost(
                    baseUri, 20, 1,
                    0, 0).runTask(); //開啟20個線程一次同時去請求這個接口
            }
        }.start();

        new Thread() {
            public void run() {
                while (isRun()) {
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("isRun:" + FlushGeneratorPost.isRun());
                    System.out
                        .println("totalThreads:" + FlushGeneratorPost.getThreadCount().totalThreads);
                    System.out.println(
                        "startedThreads:" + FlushGeneratorPost.getThreadCount().startedThreads);
                    System.out.println("runThreads:" + FlushGeneratorPost.getThreadCount().runThreads);
                    System.out.println(
                        "finishedThread:" + FlushGeneratorPost.getThreadCount().finishedThreads);
                    System.out
                        .println("successCount:" + FlushGeneratorPost.getThreadCount().successCount);
                    System.out.println("failCount:" + FlushGeneratorPost.getThreadCount().failCount);
                    System.out.println();
                }
                System.out.println("\n\n 執行" + threads * iteration + "次請求一共花費了"
                    + (System.currentTimeMillis() - startTime) / 1000 + "秒");
            }
        }.start();
    }

    /**
     * httpUrlConnection的get請求
     *
     * @param uri
     * @return
     * @throws IOException
     */
    private static Bean readContent(String uri) throws IOException {
        String body = "xxx=111&xxx22=1&xxx33=2";
        URL postUrl = new URL(uri);
        // 打開連接
        HttpURLConnection connection = (HttpURLConnection) postUrl.openConnection();

        // 設置是否向connection輸出,因為這個是post請求,參數要放在
        // http正文內,因此需要設為true
        connection.setDoOutput(true);
        // Read from the connection. Default is true.
        connection.setDoInput(true);
        // 默認是 GET方式
        connection.setRequestMethod("POST");
        connection.setConnectTimeout(3 * 1000);

        // Post 請求不能使用緩存
        connection.setUseCaches(false);

        connection.setInstanceFollowRedirects(true);

        // 配置本次連接的Content-type,配置為application/x-www-form-urlencoded的
        // 意思是正文是urlencoded編碼過的form參數,下面我們可以看到我們對正文內容使用URLEncoder.encode
        // 進行編碼
        connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
        connection.setRequestProperty("Accept", "application/json;charset=UTF-8");
        String Authorization = "xxxxxssss123sdffasdf";        connection.setRequestProperty("Authorization", Authorization);
        // 連接,從postUrl.openConnection()至此的配置必須要在connect之前完成,
        // 要注意的是connection.getOutputStream會隱含的進行connect。
        connection.connect();
        DataOutputStream out = new DataOutputStream(connection.getOutputStream());
        // The URL-encoded contend
        // 正文,正文內容其實跟get的URL中 '? '后的參數字符串一致
        //        content = "count=" + URLEncoder.encode(String.valueOf(1), "UTF-8");
        //        content +="&amount="+URLEncoder.encode(String.valueOf(10), "UTF-8");
        // DataOutputStream.writeBytes將字符串中的16位的unicode字符以8位的字符形式寫到流里面
        out.writeBytes(body);

        out.flush();
        out.close();

        BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
        String line;

        StringBuilder sb = new StringBuilder();
        while ((line = reader.readLine()) != null) {
            sb.append(line);
        }


        int state = connection.getResponseCode();
        reader.close();
        connection.disconnect();

        return new Bean(state, sb.toString());




    }

    /**
     * 結果集實體類
     */
    public static class Bean {
        private int state;
        private String result;

        public Bean(int state, String result) {
            this.state = state;
            this.result = result;
        }

        public int getState() {
            return state;
        }

        public void setState(int state) {
            this.state = state;
        }

        public String getResult() {
            return result;
        }

        public void setResult(String result) {
            this.result = result;
        }
    }
}

然后利用我的這個工具去還原請求我們被盜刷的接口.我們去一點點的驗證我們猜想的問題所在,首先,我用了一個賬戶余額充足的賬戶去測試,發現沒什么問題.
這個地方為什么沒有問題呢?我在加錢的服務和減少金錢的服務中利用了數據庫的悲觀鎖保證了用戶余額的安全(這里我們先不吐槽悲觀鎖的問題哈).
然后我又換了一個余額少一點的賬戶進行了重試,發現這次的就出問題了,加錢的用戶正確加上了20*price的金額,而扣錢的用戶賬戶為0,也就是說,假如我只有20快,但是我想花100快給a,這時候我余額變成0了(也就是花了20快),然后a的余額增加了100快,那等于系統因為bug原因陪了80快.

這時候我們再回顧一下之前我們猜測的問題

  1. 首先我的最外層的金額檢測,沒有事務和鎖的保證,所以在并發的時候,這一層的檢測就變成了無效的檢測.
  2. 檢測是否查看過由于同事的疏忽,沒有做并發時候的唯一性校驗,即如果用戶看過一次就不能再繼續再看了,所以這一層的檢測在并發的時候也會出問題.
  3. 由于并發請求使得1和2的校驗都不成功的時候這時候就來到了第三步,這時候我們是要進行真正的金額變動和變動日志的記錄了,這個時候我們看一下具體的代碼實現.

加錢服務的代碼

     * 用戶加錢的api
     *
     * @param userId                  用戶id
     * @param amount                  鉆石金額
     * @param orderId                 訂單id
     * @param orderType               訂單類型
     * @param moneyLogOrderChangeType 加錢的理由
     * @param relationUserId          這個字段意味這次加錢是跟誰有關的
     * @return
     */
    public int addUserGoldNum(String userId, int amount, String orderId, OrderType orderType,
                              MoneyLogOrderChangeType moneyLogOrderChangeType, String relationUserId) {
        if (userId == null)
            throw new BusinessException(ErrorCode.NOT_FIND_USER_ACCOUNT);
        int currentAmount = userInfoDAO.queryGoldNumByUserId(userId);
        if (amount <= 0)
            return currentAmount;
        int newAmount = currentAmount + amount;
        LOG.info(
                "<Important!> add BEGIN!! [userId]= " + userId + " [current amount]= " + currentAmount
                        + " [new ammount]= " + newAmount);
        userInfoDAO.addGoldNumByUserId(userId, amount);
        // 獲得用戶在內存中信息
        UserInfo userInfo = UserCache.getInstance().loadUserInfo(userId);
        if (userInfo != null) {
            userInfo.setGoldNum(newAmount);
            UserCache.getInstance().updateUserInfo(userInfo);
        } else
            throw new BusinessException(ErrorCode.NOT_FIND_USER);
        LOG.info("<Important!> add COMPLETE!! [userId]= " + userId + " [current amount]= "
                + currentAmount + " [new ammount]= " + newAmount);

                userService.updateUserInfo(userInfo);
        //更新data日志
        DailyUserDataLogCache.incrDiamonds(userId, amount);
        //記錄到money log
        MoneyLog moneyLog = new MoneyLog();
        moneyLog.setUserId(userId);
        moneyLog.setAmountType(MoneyLogAmountType.ADD.getType());
        moneyLog.setChangeAmount(amount);
        moneyLog.setChangeType(moneyLogOrderChangeType.getType());
        moneyLog.setChangeReason(moneyLogOrderChangeType.getReason());
        moneyLog.setOrderId(orderId);
        moneyLog.setRelationId(relationUserId);
        moneyLog.setType(orderType.getType());
        moneyLogService.save(moneyLog);

        return newAmount;

    }

減錢服務的代碼

public int subtractUserGoldNum(String userId, int amount, String orderId, OrderType orderType,
                                   MoneyLogOrderChangeType moneyLogOrderChangeType, String relationUserId) {
        if (userId == null)
            throw new BusinessException(ErrorCode.NOT_FIND_USER_ACCOUNT);
        int currentAmount = userInfoDAO.queryGoldNumByUserId(userId);
        int newAmount = currentAmount - amount;
        if (newAmount < 0)
            throw new BusinessException(ErrorCode.INSUFFICIENT_MONEY);
        LOG.info("<Important!> subtract BEGIN!! [userId]= " + userId + " [current amount]= "
                + currentAmount + " [new ammount]= " + newAmount);
        userInfoDAO.subtractGoldNumByUserId(userId, amount);
        // 獲得用戶在內存中信息
        UserInfo userInfo = UserCache.getInstance().loadUserInfo(userId);
        if (userInfo != null) {
            userInfo.setGoldNum(newAmount);
            UserCache.getInstance().updateUserInfo(userInfo);
        } else
            throw new BusinessException(ErrorCode.NOT_FIND_USER);
        LOG.info("<Important!> subtract COMPLETE!! [userId]= " + userId + " [current amount]= "
                + currentAmount + " [new ammount]= " + newAmount);

        //記錄到money log
        MoneyLog moneyLog = new MoneyLog();
        moneyLog.setUserId(userId);
        moneyLog.setAmountType(MoneyLogAmountType.SUBTRACT.getType());
        moneyLog.setChangeAmount(amount);
        moneyLog.setChangeType(moneyLogOrderChangeType.getType());
        moneyLog.setChangeReason(moneyLogOrderChangeType.getReason());
        moneyLog.setOrderId(orderId);
        moneyLog.setRelationId(relationUserId);
        moneyLog.setType(orderType.getType());
        moneyLogService.save(moneyLog);

        return newAmount;
    }

我們可以看到,按照常理來說,我們的扣錢服務中有了對余額的檢測,如果余額不夠會拋出業務異常,讓數據回滾,那么案例來說我們的程序應該是沒問題的啊?但是仔細查詢消費日志表的時候會發現,正常我加錢和扣錢都會記錄一條記錄,也就是我加錢和扣錢的記錄數量是相等的,但是在我們并發請求了之后,我的數據庫中的記錄數是不對等的,我的扣錢記錄比加錢記錄少,這是為什么呢?
其實我們之前已經說過,無論是加錢服務還是減錢服務都是有悲觀鎖來保證的,那么這個悲觀鎖是怎么回事呢,其實就是一個for update語句,在事務提交了之后會自動釋放鎖,但是由于我們的項目是一個編程式事務,而這個服務的加錢和扣錢直接在view層調用了,所以這時候這兩個服務是兩個事物,所以即使扣錢服務發生了異常,那么我們之前的錢已經給收入者加過了,這時候是無法回滾的.
然后再看一下我們的業務,重新整理思考一下邏輯,在并發請求的調用中,給用戶a加錢服務調用完之后,我們的需要調用扣錢服務給b扣費,這時候我們發現用戶余額不足了,而拋出異常,但是給a加的錢并沒有還原回去,然后b的余額也只是0而已.

解決問題

其實我們在上面的原子服務中已經做了很多的檢測,然而因為疏忽的問題造成了現在的問題,要解決這個問題有幾種辦法也都很簡單.

我們先去除view層的余額校驗(這里去除他是因為沒啥用,屬于一個優化代碼)

  1. 在檢測用戶是否查看過的地方增加鎖和唯一性校驗,保證用戶只偷看一次(這種方法其實治標不治本,也只是針對于這個接口能保證沒有問題)
  2. 把扣錢服務在加錢服務之前調用,這樣扣錢服務發送異常的時候,就會熔斷,不會繼續走加錢服務(這種方式代碼改動量最小,不過保不準哪個同事繼續會出現這樣的疏忽).
  3. 抽象出一層組合的服務層,吧扣錢和加錢放在一個事務之中,如果有其他的業務就繼續組合,讓業務處于同一個事務下,既可以保證數據安全,又能保證業務正確.還可以規范化整個開發中的代碼調用(這種方式也是我比較推薦的,而且在日后做服務化和架構梳理的時候也會比較方便,而且來了新人也不容易出問題)

總結問題

我們上面已經把問題找到并解決了問題,不過已經異常的數據還是讓本寶寶在10.24程序員節日的時候忙活了很久很久,可坑壞本寶寶了,于是乎樓主便整理了這個大事件中所暴漏出來的問題.

  1. 接口沒做簽名和加密(sign,base64沒做,客戶端未做混淆,用戶可以輕易的請求到服務接口)
  2. 相同的請求數據沒做過濾(例如加上接口調用會話id來過濾)
  3. 接口沒做組合服務(這也是整個事件過程中比較重要的一個地方,因為代碼未做規范化,所以才會暴漏了這么嚴重的問題,如果統一了服務調用就不會出現這個問題了,就像加錢和扣錢的原子服務以及處理了很多會發生的問題了)
  4. 沒做并發測試,測試點不足(不提了,可能很多公司都有這個通病吧,哎,以后要重點注意了)
  5. 用戶金錢安全性保證不夠(給不了你心愛的用戶安全感,拿什么說愛他們)
  6. 未做風控檢測(考慮每天跑跑定時任務,做一些業務檢測)
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,048評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,414評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,169評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,722評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,465評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,823評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,813評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,000評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,554評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,295評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,513評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,035評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,722評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,125評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,430評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,237評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,482評論 2 379

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,825評論 18 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,725評論 25 708
  • 關于全面推開營業稅改征增值稅試點的通知財稅〔2016〕36號 各省、自治區、直轄市、計劃單列市財政廳(局)、國家稅...
    HenryYUE閱讀 1,332評論 0 6
  • 生活總是會在你得意忘形的時候,給你重重地一擊,讓你知道,其實你仍舊是身處于無盡的泥潭里。 有許久沒有寫簡書了,重新...
    北落師門Nike閱讀 312評論 0 0
  • 1.閱讀心得 原文(一):通過修習靜坐,我們仍可以看到鏈接我們和母親的臍帶。我們看到母親不單在我們之外,也在我們之...
    寅穎閱讀 154評論 0 2