JAVA工程師常見面試題(三):mybatis緩存+手寫redis二級緩存

緩存是一般的ORM 框架都會提供的功能抄肖,目的就是提升查詢的效率和減少數(shù)據(jù)庫的壓力斯棒。跟Hibernate 一樣没龙,MyBatis 也有一級緩存二級緩存链韭,并且預留了集成第三方緩存的接口排霉。

整體架構(gòu)

image.png

一級緩存

image.png

一級緩存是在SqlSession 層面進行緩存的窍株。即,同一個SqlSession 攻柠,多次調(diào)用同一個Mapper和同一個方法的同一個參數(shù)球订,只會進行一次數(shù)據(jù)庫查詢,然后把數(shù)據(jù)緩存到緩沖中瑰钮,以后直接先從緩存中取出數(shù)據(jù)冒滩,不會直接去查數(shù)據(jù)庫。
我們來看一級緩存的觸發(fā)條件:

  • 必須是相同的SQL和參數(shù)
  • 必須是相同的會話(SqlSession)
  • 必須是相同的namespace 即同一個mapper
  • 必須是相同的statement 即同一個mapper 接口中的同一個方法
  • 查詢語句中間沒有執(zhí)行session.clearCache() 方法
  • 查詢語句中間沒有執(zhí)行 insert update delete 方法(無論變動記錄是否與 緩存數(shù)據(jù)有無關(guān)系)

必須同時滿足上述所有條件浪谴,一級緩存才會觸發(fā)开睡。

源碼案例

我們使用最新版本的spring boot構(gòu)建項目。

1.搭建spring boot+mybatis測試項目

spring官方網(wǎng)站速度太慢苟耻,切換到阿里云士八。


image.png

選擇如下的依賴:


image.png

2.連接數(shù)據(jù)庫

使用IDEA自帶的DataBase工具連接。


image.png

選擇Mysql作為數(shù)據(jù)源梁呈。


image.png

填入數(shù)據(jù)庫地址及用戶名婚度、密碼,如果提示需要下載驅(qū)動官卡,點擊下載即可蝗茁,默認選擇的mysql驅(qū)動版本為8.0+。
image.png

設(shè)置一下時區(qū)寻咒,否則8.0的驅(qū)動是無法訪問數(shù)據(jù)庫的哮翘。


image.png

這樣在右邊就可以看到我們的數(shù)據(jù)庫表了,右鍵自動生成實體類毛秘。
image.png

按照需要在這里勾選對應(yīng)的功能饭寺,然后點擊生成即可阻课。
image.png

這樣pojo、dao艰匙、mapper映射文件就自動生成了限煞。
image.png

3.配置mybatis

復制下面的配置模板到項目中,修改對應(yīng)的內(nèi)容以匹配數(shù)據(jù)庫员凝。

# 應(yīng)用名稱
spring.application.name=mybatis-test
# 應(yīng)用服務(wù) WEB 訪問端口
server.port=8080
# 數(shù)據(jù)庫驅(qū)動:
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 數(shù)據(jù)源名稱
spring.datasource.name=defaultDataSource
# 數(shù)據(jù)庫連接地址
spring.datasource.url=jdbc:mysql://localhost:3306/mp?serverTimezone=UTC
# 數(shù)據(jù)庫用戶名&密碼:
spring.datasource.username=root
spring.datasource.password=123456
#掃描映射文件路徑
mybatis.mapper-locations=classpath:/mapper/**.xml
#日志輸出
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

引導類上添加注解@MapperScan("com.brianxia.demo")

package com.brianxia.demo;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.brianxia.demo")
public class MybatisCacheTestApplication {

    public static void main(String[] args) {
        SpringApplication.run(MybatisCacheTestApplication.class, args);
    }

}

4.測試一級緩存

package com.brianxia.demo;

import com.brianxia.demo.dao.TbUserDao;
import com.brianxia.demo.pojo.TbUser;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class MybatisCacheTestApplicationTests {

    @Autowired
    private TbUserDao tbUserDao;

    //一級緩存不生效,每次查詢都會生成新的sqlsession并執(zhí)行提交署驻,不在同一個sqlsession中
    @Test
    void test1() {
        TbUser tbUser1 = tbUserDao.selectByPrimaryKey(1L);
        TbUser tbUser2 = tbUserDao.selectByPrimaryKey(1L);
        System.out.println(tbUser1 == tbUser2);
    }

}

這個時候會發(fā)現(xiàn)一級緩存并沒有生效,每次查詢都會創(chuàng)建一個新的SqlSession并發(fā)送Sql語句到mysql中健霹。


image.png

可以通過增加事務(wù)注解避免重復創(chuàng)建SqlSession會話旺上,再次進行測試:

package com.brianxia.demo;

import com.brianxia.demo.dao.TbUserDao;
import com.brianxia.demo.pojo.TbUser;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

@SpringBootTest
class MybatisCacheTestApplicationTests {

    @Autowired
    private TbUserDao tbUserDao;

    //一級緩存不生效,每次查詢都會生成新的sqlsession并執(zhí)行提交,不在同一個sqlsession中
    @Test
    @Transactional
    void test1() {
        TbUser tbUser1 = tbUserDao.selectByPrimaryKey(1L);
        TbUser tbUser2 = tbUserDao.selectByPrimaryKey(1L);
        System.out.println(tbUser1 == tbUser2);
    }

}

這次只創(chuàng)建了一個SqlSession糖埋,并且可以看到兩個對象指向同一塊堆內(nèi)存區(qū)域宣吱,所以一級緩存已經(jīng)生效。


image.png

5.測試必要條件

  • 必須是相同的SQL和參數(shù)
 //一級緩存不生效,必須是相同的SQL和參數(shù)
    @Test
    @Transactional
    void test1() {
        TbUser tbUser1 = tbUserDao.selectByPrimaryKey(1L);
        TbUser tbUser2 = tbUserDao.selectByPrimaryKey(2L);
        System.out.println(tbUser1 == tbUser2);
    }
  • 必須是相同的statement 即同一個mapper 接口中的同一個方法
    @Select("select * from tb_user where id = #{id}")
    TbUser selectByPrimaryKey2(Long id);

    //一級緩存不生效,必須是相同的statement 即同一個mapper 接口中的同一個方法
    @Test
    @Transactional
    void test2() {
        TbUser tbUser1 = tbUserDao.selectByPrimaryKey(1L);
        TbUser tbUser2 = tbUserDao.selectByPrimaryKey2(1L);
        System.out.println(tbUser1 == tbUser2);
    }
  • 查詢語句中間沒有執(zhí)行 insert update delete 方法(無論變動記錄是否與 緩存數(shù)據(jù)有無關(guān)系)
 //一級緩存不生效,查詢語句中間沒有執(zhí)行 insert update delete 方法
    @Test
    @Transactional
    void test2() {
        TbUser tbUser1 = tbUserDao.selectByPrimaryKey(1L);
        TbUser tbUser = tbUserDao.selectByPrimaryKey(2L);
        tbUserDao.updateByPrimaryKey(tbUser);
        TbUser tbUser2 = tbUserDao.selectByPrimaryKey(1L);
        System.out.println(tbUser1 == tbUser2);
    }

二級緩存

一級緩存無法實現(xiàn)在多個SqlSession中共享數(shù)據(jù)瞳别,所以mybatis提供了二級緩存征候,在SqlSessionFactory層面給各個SqlSession 對象共享。默認二級緩存是不開啟的洒试,需要手動進行配置。

1.注解方式開啟

如果使用純注解的方式朴上,首先需要在mapper接口上添加注解@CacheNamespace垒棋,這樣才能開啟二級緩存功能。

package com.brianxia.demo.dao;

import com.brianxia.demo.pojo.TbUser;
import org.apache.ibatis.annotations.CacheNamespace;
import org.apache.ibatis.annotations.CacheNamespaceRef;
import org.apache.ibatis.annotations.Select;

@CacheNamespace
public interface TbUserDao2 {

    @Select("select * from tb_user where id = #{id}")
    TbUser selectByPrimaryKey(Long id);

}

然后編寫查詢方法:

  //二級緩存
    @Test
    void test3() {
        TbUser tbUser1 = tbUserDao2.selectByPrimaryKey(1L);
        TbUser tbUser2 = tbUserDao2.selectByPrimaryKey(1L);
    }

可以觀察一下日志痪宰,應(yīng)該只有一次SQL查詢叼架,第二次SqlSession創(chuàng)建之后,會通過二級緩存查詢出數(shù)據(jù)返回衣撬。

默認的二級緩存會有如下效果

  • 映射語句文件中的所有 SELECT 語句將會被緩存乖订。
  • 映射語句文件中的所有INSERT、UPDATE具练、DELETE 語句會刷新緩存乍构。
  • 緩存會使用 Least Recently Used ( LRU,最近最少使用的)算法來收回扛点。
  • 根據(jù)時間表刷新緩存(如 no Flush Interval 哥遮,沒有刷新間隔,緩存不會以任何時間順序來刷新)陵究。
  • 緩存會存儲集合或?qū)ο螅o論查詢方法返回什么類型的值)的 1024 個引用眠饮。
  • 緩存會被視為 read/write (可讀/可寫)的,意味著對象檢索不是共享的铜邮,而且可以安全地被調(diào)用者修改仪召,而不干擾其他調(diào)用者或線程所做的潛在修改寨蹋。

接口關(guān)閉緩存

如果對于某些接口需要關(guān)閉緩存,可以在接口上通過@Options注解添加具體的關(guān)閉緩存配置項扔茅,如下:

    @Options(useCache = false)
    @Select("select * from tb_user where id = #{id}")
    TbUser selectByPrimaryKey(Long id);

這樣緩存就不會生效了已旧。

2.配置文件實現(xiàn)方式

在mapper.xml映射配置文件中,需要添加標簽<cache></cache>咖摹,這樣就可以開啟二級緩存功能评姨。

image.png

其余功能與注解方式相同。

3.配置項詳解

以注解的使用方式為例萤晴,源碼如下:

/**
 *    Copyright 2009-2020 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.apache.ibatis.annotations;

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

import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.decorators.LruCache;
import org.apache.ibatis.cache.impl.PerpetualCache;

/**
 * The annotation that specify to use cache on namespace(e.g. mapper interface).
 *
 * <p>
 * <b>How to use:</b>
 *
 * <pre>
 * &#064;CacheNamespace(implementation = CustomCache.class, properties = {
 *   &#064;Property(name = "host", value = "${mybatis.cache.host}"),
 *   &#064;Property(name = "port", value = "${mybatis.cache.port}"),
 *   &#064;Property(name = "name", value = "usersCache")
 * })
 * public interface UserMapper {
 *   // ...
 * }
 * </pre>
 *
 * @author Clinton Begin
 * @author Kazuki Shimizu
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface CacheNamespace {

  /**
   * Returns the cache implementation type to use.
   *
   * @return the cache implementation type
   */
  Class<? extends Cache> implementation() default PerpetualCache.class;

  /**
   * Returns the cache evicting implementation type to use.
   *
   * @return the cache evicting implementation type
   */
  Class<? extends Cache> eviction() default LruCache.class;

  /**
   * Returns the flush interval.
   *
   * @return the flush interval
   */
  long flushInterval() default 0;

  /**
   * Return the cache size.
   *
   * @return the cache size
   */
  int size() default 1024;

  /**
   * Returns whether use read/write cache.
   *
   * @return {@code true} if use read/write cache; {@code false} if otherwise
   */
  boolean readWrite() default true;

  /**
   * Returns whether block the cache at request time or not.
   *
   * @return {@code true} if block the cache; {@code false} if otherwise
   */
  boolean blocking() default false;

  /**
   * Returns property values for a implementation object.
   *
   * @return property values
   * @since 3.4.2
   */
  Property[] properties() default {};

}

配置項

eviction(收回策略)

  • LRU(最近最少使用的):移除最長時間不被使用的對象吐句,這是默認值。

  • FIFO(先進先出):按對象進入緩存的順序來移除它們店读。

  • SOFT(軟引用):移除基于垃圾回收器狀態(tài)和軟引用規(guī)則的對象嗦枢。

  • WEAK(弱引用):更積極地移除基于垃圾收集器狀態(tài)和弱引用規(guī)則的對象。

flushinterval(刷新間隔)

可以被設(shè)置為任意的正整數(shù)屯断,而且它們代表一個合理的毫秒形式的時間段文虏。默認情況不設(shè)置,即沒有刷新間隔殖演,緩存僅僅在調(diào)用語句時刷新氧秘。

size(引用數(shù)目)

可以被設(shè)置為任意正整數(shù),要記住緩存的對象數(shù)目和運行環(huán)境的可用內(nèi)存資源數(shù)目趴久。默認值是1024 丸相。

readOnly(只讀)

屬性可以被設(shè)置為 true 或 false。只讀的緩存會給所有調(diào)用者返回緩存對象的相同實例彼棍,因此這些對象不能被修改灭忠,這提供了很重要的性能優(yōu)勢∽叮可讀寫的緩存會通過序列化返回緩存對象的拷貝弛作,這種方式會慢一些,但是安全华匾,因此默認是 false映琳。

配置方式

@CacheNamespace(

eviction = FifoCache.class,
flushinterval = 60000,
size = 512,
readWrite = true

)

加餐1:使用redis作為二級緩存

自定義二級緩存只需要實現(xiàn)Cache接口,源碼如下:

/**
 *    Copyright 2009-2020 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.apache.ibatis.cache;

import java.util.concurrent.locks.ReadWriteLock;

/**
 * SPI for cache providers.
 * <p>
 * One instance of cache will be created for each namespace.
 * <p>
 * The cache implementation must have a constructor that receives the cache id as an String parameter.
 * <p>
 * MyBatis will pass the namespace as id to the constructor.
 *
 * <pre>
 * public MyCache(final String id) {
 *   if (id == null) {
 *     throw new IllegalArgumentException("Cache instances require an ID");
 *   }
 *   this.id = id;
 *   initialize();
 * }
 * </pre>
 *
 * @author Clinton Begin
 */

public interface Cache {

  /**
   * @return The identifier of this cache
   */
  String getId();

  /**
   * @param key
   *          Can be any object but usually it is a {@link CacheKey}
   * @param value
   *          The result of a select.
   */
  void putObject(Object key, Object value);

  /**
   * @param key
   *          The key
   * @return The object stored in the cache.
   */
  Object getObject(Object key);

  /**
   * As of 3.3.0 this method is only called during a rollback
   * for any previous value that was missing in the cache.
   * This lets any blocking cache to release the lock that
   * may have previously put on the key.
   * A blocking cache puts a lock when a value is null
   * and releases it when the value is back again.
   * This way other threads will wait for the value to be
   * available instead of hitting the database.
   *
   *
   * @param key
   *          The key
   * @return Not used
   */
  Object removeObject(Object key);

  /**
   * Clears this cache instance.
   */
  void clear();

  /**
   * Optional. This method is not called by the core.
   *
   * @return The number of elements stored in the cache (not its capacity).
   */
  int getSize();

  /**
   * Optional. As of 3.2.6 this method is no longer called by the core.
   * <p>
   * Any locking needed by the cache must be provided internally by the cache provider.
   *
   * @return A ReadWriteLock
   */
  default ReadWriteLock getReadWriteLock() {
    return null;
  }

}

從上面源碼可以分析出來蜘拉,存放和獲取的對象類型都統(tǒng)一為Object類型刊头,所以如果要將Object類型轉(zhuǎn)換為json存放到redis中會遇到反序列化類型無法獲取的問題,所以需要自定義序列化器诸尽,而不能用Json作為序列化方式原杂。
代碼如下:

package com.brianxia.demo.utils;

import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class ObjectSerializer implements RedisSerializer<Object> {

    @Override
    public byte[] serialize(Object o) throws SerializationException {
        ObjectOutputStream oos = null;
        ByteArrayOutputStream baos = null;
        try {
            baos = new ByteArrayOutputStream();
            oos = new ObjectOutputStream(baos);
            oos.writeObject(o);
            byte[] bytes = baos.toByteArray();
            return bytes;
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                if(baos != null){
                    baos.close();
                }
                if (oos != null) {
                    oos.close();
                }
            } catch (Exception e2) {
                e2.printStackTrace();
            }
        }
        return null;
    }

    /*
     * 反序列化
     * */
    public Object deserialize(byte[] bytes){
        ByteArrayInputStream bais = null;
        ObjectInputStream ois = null;
        try{
            bais = new ByteArrayInputStream(bytes);
            ois = new ObjectInputStream(bais);
            return ois.readObject();
        }catch(Exception e){
            e.printStackTrace();
        }finally {
            try {

            } catch (Exception e2) {
                e2.printStackTrace();
            }
        }
        return null;
    }

}

使用JDK的byte序列化器來進行序列化,轉(zhuǎn)換成byte[]存放到redis中您机。
創(chuàng)建RedisTemplate:

 @Bean
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<String,Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        template.setValueSerializer(new ObjectSerializer());
        template.setKeySerializer(new StringRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }

其中key的序列化器可以使用String穿肄,因為對key沒有g(shù)et還原成原始對象的操作年局,只是作為尋址參數(shù)。value的序列化器必須使用ObjectSerializer咸产,否則無法還原出原本的類型矢否。
編寫Cache接口實現(xiàn)類:

package com.brianxia.demo.cache;

import com.alibaba.fastjson.JSON;
import com.brianxia.demo.utils.RedisTemplateUtil;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheException;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.HashMap;
import java.util.Map;

/**
 * @author brianxia
 * @version 1.0
 * @date 2020/12/19 9:38
 */

public class MyCache implements Cache {

    private final String id;

    private final Map<Object, Object> cache = new HashMap<>();

    private RedisTemplate<String,Object> stringRedisTemplate;

    private String cacheKey2String(Object key){
        return JSON.toJSONString(key);
    }
    public MyCache(String id) {
        synchronized (this){
            if(stringRedisTemplate == null){
                stringRedisTemplate =  RedisTemplateUtil.redisTemplate();
            }
        }

        this.id = id;
    }

    @Override
    public String getId() {
        return id;
    }

    @Override
    public int getSize() {
        return stringRedisTemplate.opsForHash().size("testCache").intValue();
    }

    @Override
    public void putObject(Object key, Object value) {
        System.out.println("用了我自己的cache");
        stringRedisTemplate.opsForHash().put("testCache",cacheKey2String(key), value);
    }

    @Override
    public Object getObject(Object key) {
        return stringRedisTemplate.opsForHash().get("testCache",cacheKey2String(key));
    }

    @Override
    public Object removeObject(Object key) {
        return stringRedisTemplate.opsForHash().delete("testCache",cacheKey2String(key));
    }

    @Override
    public void clear() {
        stringRedisTemplate.delete("testCache");
    }

    @Override
    public boolean equals(Object o) {
        if (getId() == null) {
            throw new CacheException("Cache instances require an ID.");
        }
        if (this == o) {
            return true;
        }
        if (!(o instanceof Cache)) {
            return false;
        }

        Cache otherCache = (Cache) o;
        return getId().equals(otherCache.getId());
    }

    @Override
    public int hashCode() {
        if (getId() == null) {
            throw new CacheException("Cache instances require an ID.");
        }
        return getId().hashCode();
    }

}

  • putObject存放緩存
  • getObject獲取緩存
  • removeObject 移除緩存
  • clear清理所有緩存

這里將所有的cache存放在hash中,方便進行統(tǒng)一的管理脑溢,否則clear方法清理大量key非常損耗性能僵朗。

最后進行測試:

package com.brianxia.demo.dao;

import com.brianxia.demo.cache.MyCache;
import com.brianxia.demo.pojo.TbUser;
import org.apache.ibatis.annotations.CacheNamespace;
import org.apache.ibatis.annotations.CacheNamespaceRef;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Select;

@CacheNamespace(implementation = MyCache.class)
public interface TbUserDao2 {

    //@Options(useCache = false)
    @Select("select * from tb_user where id = #{id}")
    TbUser selectByPrimaryKey(Long id);

}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市屑彻,隨后出現(xiàn)的幾起案子验庙,更是在濱河造成了極大的恐慌,老刑警劉巖社牲,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件粪薛,死亡現(xiàn)場離奇詭異,居然都是意外死亡搏恤,警方通過查閱死者的電腦和手機违寿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來熟空,“玉大人藤巢,你說我怎么就攤上這事∠⒙蓿” “怎么了掂咒?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長阱当。 經(jīng)常有香客問我俏扩,道長糜工,這世上最難降的妖魔是什么弊添? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮捌木,結(jié)果婚禮上油坝,老公的妹妹穿的比我還像新娘。我一直安慰自己刨裆,他們只是感情好澈圈,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著帆啃,像睡著了一般瞬女。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上努潘,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天诽偷,我揣著相機與錄音坤学,去河邊找鬼。 笑死报慕,一個胖子當著我的面吹牛深浮,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播眠冈,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼飞苇,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蜗顽?” 一聲冷哼從身側(cè)響起布卡,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎诫舅,沒想到半個月后羽利,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡刊懈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年这弧,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片虚汛。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡匾浪,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出卷哩,到底是詐尸還是另有隱情蛋辈,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布将谊,位于F島的核電站冷溶,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏尊浓。R本人自食惡果不足惜逞频,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望栋齿。 院中可真熱鬧苗胀,春花似錦、人聲如沸瓦堵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽菇用。三九已至澜驮,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間惋鸥,已是汗流浹背杂穷。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工鹅龄, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人亭畜。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓扮休,卻偏偏與公主長得像,于是被迫代替她去往敵國和親拴鸵。 傳聞我的和親對象是個殘疾皇子玷坠,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容