第三章:接口冪等性校驗(yàn)的實(shí)現(xiàn)spring boot2.x+redis+注解+攔截器

引言

冪等性:就是對(duì)于用戶發(fā)起的同一操作的一次請(qǐng)求或者多次請(qǐng)求的結(jié)果是一致的,不會(huì)因?yàn)槎啻吸c(diǎn)擊而產(chǎn)生副作用。
你比如支付,在用戶購(gòu)買(mǎi)商品后的支付弦疮,支付扣款成功,但是返回結(jié)果的時(shí)候網(wǎng)絡(luò)異常蜘醋,此時(shí)錢(qián)已經(jīng)扣了胁塞,用戶再次點(diǎn)擊按鈕,此時(shí)會(huì)進(jìn)行二次付款江掩。那么不論是扣款還是流水記錄都生成了兩條記錄放仗」叮或者提交特殊訂單時(shí)網(wǎng)絡(luò)問(wèn)題生成兩筆訂單酒觅。在原來(lái)我們是把數(shù)據(jù)操作放入事務(wù)中即可危队,發(fā)生錯(cuò)誤立即回滾谊惭,但是再相應(yīng)客戶端的時(shí)候是有可能網(wǎng)絡(luò)再次中斷或者異常等空骚。

常見(jiàn)的解決方案

  • 1叠国、唯一索引:防止新增臟數(shù)據(jù)
  • 2衩匣、token機(jī)制:防止頁(yè)面重復(fù)提交
  • 3蕾总、悲觀鎖: 獲取數(shù)據(jù)的時(shí)候加鎖(鎖表或鎖行)
  • 4、樂(lè)觀鎖:基于版本號(hào)version實(shí)現(xiàn)琅捏,在更新數(shù)據(jù)的那一刻校驗(yàn)數(shù)據(jù)
  • 5生百、分布式鎖:redis(jedis、redisson)或zookeeper實(shí)現(xiàn)

Jedis是Redis官方推薦的Java連接開(kāi)發(fā)工具

Redisson是架設(shè)在Redis基礎(chǔ)上的一個(gè)Java駐內(nèi)存數(shù)據(jù)網(wǎng)格(In-Memory Data Grid)柄延∈唇【Redis官方推薦】

ZooKeeper是一個(gè)分布式的,開(kāi)放源碼的分布式應(yīng)用程序協(xié)調(diào)服務(wù)搜吧,是Google的Chubby一個(gè)開(kāi)源的實(shí)現(xiàn)市俊,是Hadoop和Hbase的重要組件。它是一個(gè)為分布式應(yīng)用提供一致性服務(wù)的軟件赎败,提供的功能包括:配置維護(hù)秕衙、域名服務(wù)、分布式同步僵刮、組服務(wù)等据忘。

  • 6、狀態(tài)機(jī):狀態(tài)變更搞糕,更新數(shù)據(jù)時(shí)判斷狀態(tài)

實(shí)現(xiàn)

本文采用第2中實(shí)現(xiàn)方式勇吊,即通過(guò)redis + token機(jī)制實(shí)現(xiàn)接口冪等性校驗(yàn)

思路

為需要保證冪等性的每一次請(qǐng)求創(chuàng)建一個(gè)唯一標(biāo)識(shí)token,先獲取token窍仰,并將此token存入redis,請(qǐng)求接口時(shí)汉规,將此token放到header或者作為請(qǐng)求參數(shù)請(qǐng)求接口,后端接口判斷redis中是否存在此token:

  • 如果存在驹吮,正常處理業(yè)務(wù)邏輯针史,并從redis中刪除此token,如果是重復(fù)請(qǐng)求碟狞,由于token已被刪除啄枕,則不能通過(guò)校驗(yàn),返回請(qǐng)勿重復(fù)操作提示
  • 如果不存在族沃,說(shuō)明參數(shù)不合法或者是重復(fù)請(qǐng)求频祝,返回提示

應(yīng)用

用到的開(kāi)發(fā)工具

開(kāi)發(fā)
直接貼pom.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.jinzheyi</groupId>
    <artifactId>idempotency</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>
    <name>idempotency</name>
    <description>接口冪等性校驗(yàn)</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!--redis-->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.4</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

然后我們可以配置我們的redis泌参。配置redis之前需要安裝一下redis。具體安裝過(guò)程就不描述了常空。
application.yml配置如下

#設(shè)置端口號(hào)
server:
  port: 1111

#redis config
spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password: password
    jedis:
      pool:
        max-idle: 8
        max-wait: -1
        min-idle: 0
    timeout: 0

配置redis

package com.jinzheyi.idempotency.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

/**
 * @version 0.1
 * @Description jedis配置類(lèi)
 * @Author jinzheyi
 * @Date 2020/2/29 12:43
 */
@Configuration
public class JedisConfig {

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private int port;

    @Value("${spring.redis.password}")
    private String password;

    @Value("${spring.redis.jedis.pool.max-idle}")
    private int maxIdle;

    @Value("${spring.redis.jedis.pool.min-idle}")
    private int minIdle;

    @Value("${spring.redis.jedis.pool.max-wait}")
    private long maxWait;

    @Value("${spring.redis.timeout}")
    private int timeout;

    @Bean
    public JedisPool redisPoolFactory(){
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxIdle(maxIdle);
        jedisPoolConfig.setMaxWaitMillis(maxWait);
        jedisPoolConfig.setMinIdle(minIdle);
        JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, password);
        return jedisPool;
    }


}

然后創(chuàng)建jedisUtil用于操作redis的工具類(lèi)

package com.jinzheyi.idempotency.utils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

/**
 *@Description redis連接Java的開(kāi)發(fā)工具類(lèi)
 *@Author jinzheyi
 *@Date 2020/2/25 20:54
 *@version 0.1
 */
@Component
public class JedisUtil {

    private static final Logger logger = LoggerFactory.getLogger(JedisUtil.class);

    @Autowired
    private JedisPool jedisPool;

    private Jedis getJedis(){
        return jedisPool.getResource();
    }

    /**
     * 設(shè)值
     * @param key 鍵
     * @param value 值
     * @return
     */
    public String set(String key, String value){
        Jedis jedis = null;
        try {
            jedis = getJedis();
            return jedis.set(key,value);
        }catch (Exception e) {
            logger.error("設(shè)置key:{} value:{} 錯(cuò)誤",key, value, e);
            return null;
        }finally {
            close(jedis);
        }
    }

    /**
     * 設(shè)置帶過(guò)期時(shí)間的值
     * @param key 鍵
     * @param value 值
     * @param expireTime 過(guò)期時(shí)間沽一,單位 s
     * @return
     */
    public String setex(String key, String value, int expireTime){
        Jedis jedis = null;
        try {
            jedis = getJedis();
            return jedis.setex(key, expireTime, value);
        }catch (Exception e){
            logger.error("設(shè)置 key:{} value: {} expireTime: {} 錯(cuò)誤", key, value, expireTime);
            return null;
        }finally {
            close(jedis);
        }
    }

    /**
     * 取值
     * @param key
     * @return
     */
    public String get(String key){
        Jedis jedis = null;
        try {
            jedis = getJedis();
            return jedis.get(key);
        } catch (Exception e){
            logger.error("取值 key:{} 錯(cuò)誤",key,e);
            return null;
        } finally {
            close(jedis);
        }
    }

    /**
     * 刪除 key
     * @param key
     * @return
     */
    public Long del(String key){
        Jedis jedis = null;
        try {
            jedis = getJedis();
            return jedis.del(key.getBytes());
        } catch (Exception e) {
            logger.error("刪除 key:{} 錯(cuò)誤",key, e);
            return null;
        } finally {
            close(jedis);
        }
    }

    /**
     * 判斷key是否存在
     * @return
     */
    public Boolean exist(String key){
        Jedis jedis = null;
        try {
            jedis = getJedis();
            return jedis.exists(key.getBytes());
        } catch (Exception e) {
            logger.error("判斷key:{} 是否存在錯(cuò)誤",key, e);
            return null;
        } finally {
            close(jedis);
        }
    }

    /**
     * 設(shè)置指定key的過(guò)期時(shí)間
     * @param key
     * @param expireTime
     * @return
     */
    public Long expire(String key, int expireTime){
        Jedis jedis = null;
        try {
            jedis = getJedis();
            return jedis.expire(key.getBytes(),expireTime);
        } catch (Exception e) {
            logger.error("設(shè)置 key:{}的過(guò)期時(shí)間 expireTime:{} 錯(cuò)誤",key,expireTime, e);
            return null;
        } finally {
            close(jedis);
        }
    }

    /**
     * 獲取key的剩余時(shí)間
     * @param key
     * @return
     */
    public Long ttl(String key){
        Jedis jedis = null;
        try {
            jedis = getJedis();
            return jedis.ttl(key.getBytes());
        } catch (Exception e) {
            logger.error("獲取 key:{}的剩余時(shí)間錯(cuò)誤",key, e);
            return null;
        } finally {
            close(jedis);
        }
    }

    /**
     * 對(duì)jedis進(jìn)行關(guān)閉
     * @param jedis
     */
    private void close(Jedis jedis){
        if(null != jedis){
            jedis.close();
        }
    }

}

我們用到了token機(jī)制來(lái)檢驗(yàn),那必然需要?jiǎng)?chuàng)建先進(jìn)行創(chuàng)建token漓糙,后期也需要校驗(yàn)token是否一致以及是否存在铣缠,創(chuàng)建業(yè)務(wù)處理接口類(lèi)和對(duì)其實(shí)現(xiàn)
接口

package com.jinzheyi.idempotency.service;

import com.jinzheyi.idempotency.common.ResponseData;
import javax.servlet.http.HttpServletRequest;

/**
 * @version 0.1
 * @Description TODO
 * @Author jinzheyi
 * @Date 2020/2/25 21:38
 */
public interface TokenService {

    ResponseData createToken();

    void checkToken(HttpServletRequest request);
}

接口實(shí)現(xiàn)

package com.jinzheyi.idempotency.service.impl;

import com.jinzheyi.idempotency.common.Constant;
import com.jinzheyi.idempotency.common.ResponseCode;
import com.jinzheyi.idempotency.common.ResponseData;
import com.jinzheyi.idempotency.exception.ServiceException;
import com.jinzheyi.idempotency.service.TokenService;
import com.jinzheyi.idempotency.utils.JedisUtil;
import com.jinzheyi.idempotency.utils.RandomUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.text.StrBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;

/**
 * @version 0.1
 * @Description TODO
 * @Author jinzheyi
 * @Date 2020/2/25 21:41
 */
@Service
public class TokenServiceImpl implements TokenService {

    private static final String TOKEN_NAME = "token";

    @Autowired
    private JedisUtil jedisUtil;

    @Override
    public ResponseData createToken() {
        String str = RandomUtil.UUID32();
        StrBuilder token = new StrBuilder();
        token.append(Constant.TOKEN_PREFIX).append(str);
        jedisUtil.setex(token.toString(), token.toString(), Constant.EXPIRE_TIME_HOUR);
        return ResponseData.success(token.toString());
    }

    @Override
    public void checkToken(HttpServletRequest request) {
        String token = request.getHeader(TOKEN_NAME);
        if (StringUtils.isBlank(token)) {// header中不存在token
            token = request.getParameter(TOKEN_NAME);
            if (StringUtils.isBlank(token)) {// parameter中也不存在token
                throw new ServiceException(ResponseCode.ILLEGAL_ARGUMENT.getMessage());
            }
        }
        if (!jedisUtil.exist(token)) {
            throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getMessage());
        }
        Long del = jedisUtil.del(token);
        if (del <= 0) {
            throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getMessage());
        }
    }

}

我們可以自定義接口冪等性的注解@ApiIdempotent,在需要接口冪等性的方法上加上此注解兼蜈,對(duì)有此注解的方法才進(jìn)行判斷

package com.jinzheyi.idempotency.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @version 0.1
 * @Description 自定義接口冪等性的注解
 * @Author jinzheyi
 * @Date 2020/2/25 20:56
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIdempotent {
}

創(chuàng)建自定義攔截器來(lái)做公共的判斷攘残。實(shí)現(xiàn)統(tǒng)一入口來(lái)校驗(yàn)

package com.jinzheyi.idempotency.interceptor;

import com.jinzheyi.idempotency.annotation.ApiIdempotent;
import com.jinzheyi.idempotency.service.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

/**
 * @version 0.1
 * @Description 接口冪等性攔截器
 * @Author jinzheyi
 * @Date 2020/2/25 21:02
 */
public class ApiIdempotentInterceptor extends HandlerInterceptorAdapter {

    @Autowired
    private TokenService tokenService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if(!(handler instanceof HandlerMethod)){
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        ApiIdempotent apiIdempotent = method.getAnnotation(ApiIdempotent.class);
        if(apiIdempotent!=null){
            check(request); //當(dāng)方法上加了需要校驗(yàn)接口冪等性的注解時(shí)進(jìn)行校驗(yàn)拙友,并統(tǒng)一友好的返回界面
        }
        return true;
    }

    private void check(HttpServletRequest request){
        tokenService.checkToken(request);
    }
}
package com.jinzheyi.idempotency.interceptor;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @version 0.1
 * @Description 相關(guān)配置類(lèi)
 * @Author jinzheyi
 * @Date 2020/2/27 22:20
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(apiIdempotentInterceptor());
    }

    /**
     * 解決跨域
     * @param registry
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")    //允許的來(lái)源
                .allowCredentials(true)   //允許的憑證
                .allowedMethods("GET","POST","DELETE","PUT","PATCH","OPTIONS","HEAD")
                .maxAge(3600 * 24);
    }

    /**
     * 自定義攔截器注入bean中
     * @return
     */
    @Bean
    public ApiIdempotentInterceptor apiIdempotentInterceptor(){
        return new ApiIdempotentInterceptor();
    }
}

最后我們建立一個(gè)用于測(cè)試的業(yè)務(wù)處理類(lèi)

package com.jinzheyi.idempotency.controller;

import com.jinzheyi.idempotency.annotation.ApiIdempotent;
import com.jinzheyi.idempotency.common.ResponseData;
import com.jinzheyi.idempotency.service.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @version 0.1
 * @Description 用于測(cè)試
 * @Author jinzheyi
 * @Date 2020/2/27 22:30
 */
@RestController
@RequestMapping("/test")
public class TestController {

    @Autowired
    private TokenService tokenService;

    @GetMapping("/getToken")
    public ResponseData getToken(){
        return tokenService.createToken();
    }

    @ApiIdempotent
    @PostMapping("/idempotent")
    public ResponseData idempotent(){
        return ResponseData.success("調(diào)用成功");
    }
}

代碼里面的封裝捕獲的自定義異常處理以及自定義返回統(tǒng)一格式封裝可以到我的gitee
里面具體查看怎么處理

測(cè)試

程序啟動(dòng)之后为狸,我們先用postman工具請(qǐng)求token

postman請(qǐng)求獲取token

然后打開(kāi)壓力測(cè)試工具,我們前期模擬50個(gè)用戶來(lái)并發(fā)請(qǐng)求 http://localhost:1111/test/idempotent
設(shè)置50個(gè)并發(fā)線程請(qǐng)求

線程組參數(shù)詳解

  • 線程數(shù):虛擬用戶數(shù)遗契。一個(gè)虛擬用戶占用一個(gè)進(jìn)程或線程辐棒。設(shè)置多少虛擬用戶數(shù)在這里也就是設(shè)置多少個(gè)線程數(shù)。
  • Ramp-Up Period(in seconds)準(zhǔn)備時(shí)長(zhǎng):設(shè)置的虛擬用戶數(shù)需要多長(zhǎng)時(shí)間全部啟動(dòng)牍蜂。如果線程數(shù)為10漾根,準(zhǔn)備時(shí)長(zhǎng)為2,那么需要2秒鐘啟動(dòng)10個(gè)線程鲫竞,也就是每秒鐘啟動(dòng)5個(gè)線程辐怕。
  • 循環(huán)次數(shù):每個(gè)線程發(fā)送請(qǐng)求的次數(shù)。如果線程數(shù)為10从绘,循環(huán)次數(shù)為100寄疏,那么每個(gè)線程發(fā)送100次請(qǐng)求〗┚總請(qǐng)求數(shù)為10*100=1000 陕截。如果勾選了“永遠(yuǎn)”,那么所有線程會(huì)一直發(fā)送請(qǐng)求批什,一到選擇停止運(yùn)行腳本农曲。
  • Delay Thread creation until needed:直到需要時(shí)延遲線程的創(chuàng)建。
  • 調(diào)度器:設(shè)置線程組啟動(dòng)的開(kāi)始時(shí)間和結(jié)束時(shí)間(配置調(diào)度器時(shí)驻债,需要勾選循環(huán)次數(shù)為永遠(yuǎn))
    持續(xù)時(shí)間(秒):測(cè)試持續(xù)時(shí)間乳规,會(huì)覆蓋結(jié)束時(shí)間
    啟動(dòng)延遲(秒):測(cè)試延遲啟動(dòng)時(shí)間,會(huì)覆蓋啟動(dòng)時(shí)間
    啟動(dòng)時(shí)間:測(cè)試啟動(dòng)時(shí)間合呐,啟動(dòng)延遲會(huì)覆蓋它暮的。當(dāng)啟動(dòng)時(shí)間已過(guò),手動(dòng)只需測(cè)試時(shí)當(dāng)前時(shí)間也會(huì)覆蓋它合砂。
    結(jié)束時(shí)間:測(cè)試結(jié)束時(shí)間青扔,持續(xù)時(shí)間會(huì)覆蓋它源织。
    傳入提前獲取的token來(lái)請(qǐng)求

    第一條成功的請(qǐng)求

    后續(xù)失敗的請(qǐng)求

    重要注意點(diǎn)(敲黑板)
    校驗(yàn)是否刪除成功

    上圖中, 不能單純的直接刪除token而不校驗(yàn)是否刪除成功, 會(huì)出現(xiàn)并發(fā)安全性問(wèn)題, 因?yàn)? 有可能多個(gè)線程同時(shí)走到第46行, 此時(shí)token還未被刪除, 所以繼續(xù)往下執(zhí)行, 如果不校驗(yàn)jedisUtil.del(token)的刪除結(jié)果而直接放行, 那么還是會(huì)出現(xiàn)重復(fù)提交問(wèn)題, 即使實(shí)際上只有一次真正的刪除操作。

作者:金哲一(jinzheyi)【筆名】
本文代碼地址:https://gitee.com/jinzheyi/springboot/tree/master/springboot2.x/idempotency-3
本文鏈接:http://www.reibang.com/p/3461441f9779

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末微猖,一起剝皮案震驚了整個(gè)濱河市谈息,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌凛剥,老刑警劉巖侠仇,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異犁珠,居然都是意外死亡逻炊,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門(mén)犁享,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)余素,“玉大人,你說(shuō)我怎么就攤上這事炊昆〗暗酰” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵凤巨,是天一觀的道長(zhǎng)视乐。 經(jīng)常有香客問(wèn)我,道長(zhǎng)敢茁,這世上最難降的妖魔是什么佑淀? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮彰檬,結(jié)果婚禮上伸刃,老公的妹妹穿的比我還像新娘。我一直安慰自己僧叉,他們只是感情好奕枝,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著瓶堕,像睡著了一般隘道。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上郎笆,一...
    開(kāi)封第一講書(shū)人閱讀 49,741評(píng)論 1 289
  • 那天谭梗,我揣著相機(jī)與錄音,去河邊找鬼宛蚓。 笑死激捏,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的凄吏。 我是一名探鬼主播远舅,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼闰蛔,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了图柏?” 一聲冷哼從身側(cè)響起序六,我...
    開(kāi)封第一講書(shū)人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蚤吹,沒(méi)想到半個(gè)月后例诀,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,104評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡裁着,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年繁涂,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片二驰。...
    茶點(diǎn)故事閱讀 38,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡扔罪,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出诸蚕,到底是詐尸還是另有隱情步势,我是刑警寧澤,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布背犯,位于F島的核電站,受9級(jí)特大地震影響盅抚,放射性物質(zhì)發(fā)生泄漏漠魏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一妄均、第九天 我趴在偏房一處隱蔽的房頂上張望柱锹。 院中可真熱鬧,春花似錦丰包、人聲如沸禁熏。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)瞧毙。三九已至,卻和暖如春寄症,著一層夾襖步出監(jiān)牢的瞬間宙彪,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工有巧, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留释漆,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓篮迎,卻偏偏與公主長(zhǎng)得像男图,于是被迫代替她去往敵國(guó)和親示姿。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348