Java详解:基于Spring的Web缓存

缓存的基本思想其实是以空间换时间。我们知道,IO的读写速度相对内存来说是非常比较慢的,通常一个web应用的瓶颈就出现在磁盘IO的读写上。那么,如果我们在内存中建立一个存储区,将数据缓存起来,当浏览器端由请求到达的时候,直接从内存中获取相应的数据,这样一来可以降低服务器的压力,二来,可以提高请求的响应速度,提升用户体验。

缓存的分类

· 数据库数据缓存
一般来说,web应用业务逻辑业务逻辑比较复杂,数据库繁多,要获取某个完整的数据,往往要多次读取数据库,或者使用极其复杂效率较低的SQL查询语句。为了提高查询的性能,将查询后的数据放到内存中进行缓存,下次查询时,直接从内存缓存直接返回,提高响应效率。
· 应用层缓存
应用层缓存主要针对某个业务方法进行缓存,有些业务对象逻辑比较复杂,,可能涉及到多次数据库读写或者其他消耗较高的操作,应用层缓存可以将复杂的业务逻辑解放出来,降低服务器压力。
· 页面缓存
除了IO外,web应用的另一大瓶颈就是页面模板的渲染。每次请求都需要从业务逻辑层获取相应的model,并将其渲染成对应的HTML。一般来说,web应用读取数据的需求比更新数据的需求大很多,大多数情况下,某个请求返回的HTML是一样的,因此直接将HTML缓存起来也是缓存的一个主流做法。
· 代理服务器缓存
代理服务器是浏览器和源服务器之间的中间服务器,浏览器先向这个中间服务器发起Web请求,经过处理后(比如权限验证,缓存匹配等),再将请求转发到源服务器。代理服务器缓存的运作原理跟浏览器的运作原理差不多,只是规模更大。可以把它理解为一个共享缓存,不只为一个用户服务,一般为大量用户提供服务,因此在减少相应时间和带宽使用方面很有效,同一个副本会被重用多次。
· CDN缓存
CDN( Content delivery networks )缓存,也叫网关缓存、反向代理缓存。浏览器先向CDN网关发起Web请求,网关服务器后面对应着一台或多台负载均衡源服务器,会根据它们的负载请求,动态将请求转发到合适的源服务器上。虽然这种架构负载均衡源服务器之间的缓存没法共享,但却拥有更好的处扩展性。
顺便给大家推荐一个Java技术交流群:473984645里面会分享一些资深架构师录制的视频资料:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多!

基于spring的缓存

spring作为一个成熟的java web 框架,自身有一套完善的缓存机制,同时,spring还未其他缓存的实现提供了扩展。接下来,让我们在一个简单的学生管理系统中尝试spring的数据库缓存、应用层缓存、页面缓存的实现。

基于spring的Web缓存

源程序简介

数据库准备

测试程序使用了mysql作为数据库,安装好mysql后,建立一个空白的 数据库,例如cache。
建好数据库后,修改src/main/resources/application.properties的数据库配置

<code>spring.datasource.url=jdbc:mysql://localhost/cache?useUnicode=true&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=
</code>

利用maven启动程序

该系统利用maven作为构建工具,如果对maven没有了解的同学可以自行了解一下,我们会利用maven进行整个项目的构建以及运行。因此需要大家下载安装maven。
安装完成后,打开命令行,进入程序所在目录,输入以下命令:

<code>mvn spring-boot:run
</code>

直接运行com.tmy.App.java

如果你成功的将项目作为一个maven项目导入进eclipse,直接运行com.tmy.App.java也可以将项目启动起来。
注意,如果希望将项目导入进eclipse,需要为eclipse添加maven插件,否则会出现依赖的类找不到的问题。

页面列表

以下是程序所提供的所有页面以及相关说明:

<code>http://localhost:8111/blogs //没有加缓存的博客列表页面
http://localhost:8111/blogs/dao //添加了数据层缓存
http://localhost:8111/blogs/service?test=test //添加了服务层缓存
http://localhost:8111/blogs/service/update?test=test //更新服务层缓存
http://localhost:8111/blogs/service/evict?test=test //删除服务层缓存
http://localhost:8111/blogs/service/test?test=test //删除服务层缓存的同时更新缓存
http://localhost:8111/blogs/page //添加了页面缓存
http://localhost:8111/blogs/page/update //清空页面缓存
http://localhost:8111/blogs/page/delete //清空页面缓存
</code>

涉及到的技术

· maven
maven是目前主流java的构建工具之一,如果对maven没有了解的同学可以自行了解一下,接下来我们会利用maven进行整个项目的构建以及运行。
· spring boot
spring boot是spring的一个子项目,其目的是spring应用的初始搭建以及开发过程。
· Spring
Spring作为目前主流的java web框架,大家应该都很了解,这里不做过多介绍。
· JPA
JPA全称[JavaPersistence API,JPA通过JDK 5.0注解或XML描述对象-关系表的映射关系,并将运行期的实体对象持久化到数据库中。
顺便给大家推荐一个Java技术交流群:473984645里面会分享一些资深架构师录制的视频资料:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多!
· EhCache
EhCache 是一个纯Java的进程内缓存框架,具有快速、精干等特点。我们的学生管理系统将利用EhCache对数据库层进行缓存。

​配置EhCache

对EhCache的依赖

这里我们主要的依赖是指对EhCache的依赖,需要在Spring项目中引入EhCache,在pom.xml中加入以下代码即可:

<code>    <dependency>
        <groupid>org.hibernate</groupid>
        hibernate-ehcache</artifactid>
    </dependency>
</code>

配置CacheManager

添加ehcache配置文件

在src/main/resources下添加文件ehcache.xml:

<code><!--?xml version="1.0" encoding="UTF-8"?-->
<ehcache xsi:nonamespaceschemalocation="[http://ehcache.org/ehcache.xsd](http://ehcache.org/ehcache.xsd)" xmlns:xsi="[http://www.w3.org/2001/XMLSchema-instance](http://www.w3.org/2001/XMLSchema-instance)" updatecheck="false" name="CM1" maxbyteslocalheap="16M">
    <diskstore path="/data/app/cache/ehcache">
    <defaultcache timetoliveseconds="36000" timetoidleseconds="3600" overflowtodisk="false" maxelementsinmemory="10000" eternal="false">
</defaultcache></diskstore></ehcache>
</code>

encache可以对以下参数进行配置:
· name
缓存名称
· maxElementsInMemory
内存中最大缓存对象数
· maxElementsOnDisk
硬盘中最大缓存对象数,若是0表示无穷大
· eternal
true表示对象永不过期,此时会忽略timeToIdleSeconds和timeToLiveSeconds属性,默认为false
· overflowToDisk
true表示当内存缓存的对象数目达到了maxElementsInMemory界限后,会把溢出的对象写到硬盘缓存中。注意:如果缓存的对象要写入到硬盘中的话,则该对象必须实现了Serializable接口才行。
· diskSpoolBufferSizeMB
磁盘缓存区大小,默认为30MB。每个Cache都应该有自己的一个缓存区。
· diskPersistent
是否缓存虚拟机重启期数据
· ​diskExpiryThreadIntervalSeconds
磁盘失效线程运行时间间隔,默认为120秒
· timeToIdleSeconds
设定允许对象处于空闲状态的最长时间,以秒为单位。当对象自从最近一次被访问后,如果处于空闲状态的时间超过了timeToIdleSeconds属性值,这个对象就会过期,EHCache将把它从缓存中清空。只有当eternal属性为false,该属性才有效。如果该属性值为0,则表示对象可以无限期地处于空闲状态
· timeToLiveSeconds
设定对象允许存在于缓存中的最长时间,以秒为单位。当对象自从被存放到缓存中后,如果处于缓存中的时间超过了 timeToLiveSeconds属性值,这个对象就会过期,EHCache将把它从缓存中清除。只有当eternal属性为false,该属性才有效。如果该属性值为0,则表示对象可以无限期地存在于缓存中。timeToLiveSeconds必须大于timeToIdleSeconds属性,才有意义
· memoryStoreEvictionPolicy
当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)、LFU(最少访问次数)。
顺便给大家推荐一个Java技术交流群:473984645里面会分享一些资深架构师录制的视频资料:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多!

添加cacheManager

首先,我们要通过@EnableCaching标注将Spring通过标注进行缓存管理的功能打开,以方便我们之后通过标注添加数据库缓存。
然后,为CacheConfiguration添加@Configuration标注,打开CacheConfiguration内@Bean的功能。
生成一个CacheManager的实例。
最后,在web app销毁的时候销毁cacheManager。

<code>@Configuration
@EnableCaching
public class CacheConfiguration {
    private net.sf.ehcache.CacheManager cacheManager;
    @PreDestroy
    public void destroy() {
        cacheManager.shutdown();
    }
    @Bean
    public CacheManager cacheManager() {
        cacheManager = net.sf.ehcache.CacheManager.create()
        EhCacheCacheManager ehCacheManager = new EhCacheCacheManager();
        ehCacheManager.setCacheManager(cacheManager);
        return ehCacheManager;
    }
}
</code>

数据层缓存实现

添加ehcache设置

首先,我们需要在EhCache中设置一块区域来存放缓存,在src/main/resources/ehcache.xml中添加如下配置:

<code><cache name="com.tmy.model.User"></cache>
<cache name="com.tmy.model.Blog"></cache>
</code>

Hibernate的一级缓存和二级缓存

Hibernate提供了两级缓存,第一级是Session的缓存。由于Session对象的生命周期通常对应一个数据库事务或者一个应用事务,因此它的缓存是事务范围的缓存。第一级缓存是必需的,hibernate会默认提供好。
第二级缓存是一个可插拔的的缓存插件,它是由SessionFactory负责管理。由于SessionFactory对象的生命周期和应用程序的整个过程对应,因此第二级缓存是进程范围或者集群范围的缓存。这个缓存中存放的对象的松散数据第二级缓存是可选的,可以在每个类或每个集合的粒度上配置第二级缓存。

打开二级缓存

我们可以通过为entry对象添加标注的方式打开二级缓存:

<code>@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
</code>

二级缓存一共有以下5种策略:
· CacheConcurrencyStrategy.NONE
不使用缓存,默认的缓存策略
· CacheConcurrencyStrategy.READ_ONLY
只读模式,在此模式下,如果对数据进行更新操作,会有异常
· CacheConcurrencyStrategy.READ_WRITE
读写模式在更新缓存的时候会把缓存里面的数据换成一个锁,其它事务如果去取相应的缓存数据,发现被锁了,直接就去数据库查询
· CacheConcurrencyStrategy.NONSTRICT_READ_WRITE
不严格的读写模式则不会的缓存数据加锁
· CacheConcurrencyStrategy.TRANSACTIONAL
事务模式指缓存支持事务,当事务回滚时,缓存也能回滚

指定cache region factory

然后,在src/main/resources/application.properties中为cache指定一个factory:

<code>spring.jpa.properties. =org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory
</code>

性能对比

第一次访问

第一次访问http://localhost:8111/blogs时,waiting也就是服务器响应的时间为2.82秒,耗时较多。
注意:这里消耗2.82秒的原因是:在Blog对象中添加了对成员creator添加了@ManyToOne的标注,因此,当通过JPA获取blog对象后,JPA还会请求一次SQL查询,去user表中获取user信息,将user填充进来,而为了效果更加明显,系统在添加测试数据时为每个blog都添加了不同的user,导致sql请求大大增加,处理时间也大大增加

多次访问未缓存页面

多次访问http://localhost:8111/blogs后,服务器响应时间大大减少,基本保持在700毫秒左右:
这是因为mysql实际上帮我们做了缓存的工作,因此,多次访问后,服务器响应时间会大大减少。如果大家有兴趣,可以自行搜索mysql缓存相关的内容。

多次访问已缓存页面

那么,在多次访问http://localhost:8111/blogs/dao后,访问时间基本保持在100多毫秒,比没有缓存的页面效率高了5倍左右,比第一次访问效率高了20倍以上。

服务层缓存实现

Spring缓存的相关标注

Spring 提供了一套标注来保住我们快速的实现缓存系统:
· @Cacheable触发添加缓存的方法
· @CacheEvict触发删除缓存的方法
· @CachePut在不干涉方法执行的情况下更新缓存
· @Caching组织多个缓存标注的标注
· @CacheConfig在class的层次共享缓存的设置
接下来我们来看缓存的具体实现。

添加ehcache设置

和数据层缓存一样,需要在内存中设置一块区域来存放service的缓存,在src/main/resources/ehcache.xml中添加如下配置:

<code><cache name="com.tmy.service.allBlogs"></cache>
</code>

顺便给大家推荐一个Java技术交流群:473984645里面会分享一些资深架构师录制的视频资料:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多!

为某个方法添加缓存

首先,在BlogWithCacheService上添加@CacheConfig(cacheNames = "com.tmy.service.allBlogs")标注,表明在BlogWithCacheService中的方法的缓存都是放在com.tmy.service.allBlogs区域中。
在需要缓存的方法上添加@Cacheable标注:

<code>@Cacheable(key = "#justTest")
public List<blogwithoutcache> findAll(String justTest){
    return blogRepository.findAll()
}
</blogwithoutcache></code>

当第一次调用该方法后,其返回值就会添加进缓存当中,当第二次调用时就能直接从缓存中获取对象了。为了测试缓存功能,我们为findAll方法添加了一个参数,这里我们将这个参数作为缓存的key。除了用参数之外,Spring还提供了其他解析方式来生成key:
· 被调用方法的名称#root.methodName
· 被调用的方法#root.method.name
· 被调用的目标对象#root.target
· 被调用的对象的类名#root.targetClass
· 被调用方法的参数#root.args[0]
· 被调用方法所用的缓存#root.caches[0].name
· 被调用方法的参数名#arg
· 调用后的结果(该参数只在unless参数或者@CachePut标注中才能使用)#result
顺便给大家推荐一个Java技术交流群:473984645里面会分享一些资深架构师录制的视频资料:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多!

更新缓存

添加进缓存后,在update方法中添加@CachePut标注可以更新相应的缓存,同样,我们还是使用传进来的参数来更新相应的缓存:

<code>@CachePut(key = "#justTest")
public List<blogwithoutcache> updateAll(String justTest){
    BlogWithoutCache blog = new BlogWithoutCache();
    blog.setContent("这是不存在的博客");
    blog.setTitle("谨慎使用这个方法")
    return Lists.newArrayList(blog);
}
</blogwithoutcache></code>

删除缓存

在某些情况下,我们还需要删除缓存,@CacheEvict可以干这件事情:

<code>@CacheEvict(key = "#justTest")
public void evictAll(String justTest){
}
</code>

组织多种缓存操作

如果你想在一个方法中同时对缓存做多种操作,Spring支持使用@Caching来组织这些操作:

<code>@Caching(evict = @CacheEvict(key="#justTest"), put = @CachePut(key="test"))
public List<blogwithoutcache> testForCaching(String justTest){
    BlogWithoutCache blog = new BlogWithoutCache();
    blog.setContent("这是不存在的博客");
    blog.setTitle("谨慎使用这个方法");
    return Lists.newArrayList(blog);
}
</blogwithoutcache></code>

性能对比

多次访问service层缓存页面

在多次访问http://localhost:8111/blogs/service?test=test后,服务器的访问时间基本保持在100毫秒以下,根据上次实验可以发现,其效率甚至比加了数据层缓存后还要高。

新缓存

更新缓存前,访问http://localhost:8111/blogs/service?test=test页面,看下以下博客:
访问http://localhost:8111/blogs/service/update?test=test,将发现数据库没有变化,但是返回的博客列表发生了变化:

删除缓存

现在缓存对象已经被玩坏了,让我们访问http://localhost:8111/blogs/service/evict?test=test缓存的对象给删掉,再次访问http://localhost:8111/blogs/service/update?test=test,我们发现博客列表重新变为正确的列表,同时服务器响应时间变成和没有做缓存时一致:

页面缓存的实现

添加缓存空间

同样,第一件事情让我们添加一下缓存的空间:

<code><cache name="com.tmy.mapper.allBlogs"></cache>
</code>

PageCachingFilter

ehcache为我们提供了几个缓存页面的filter,使用这些filter实现缓存:
· SimplePageCachingFilter
最基本的页面缓存filter实现,其满足大部分页面缓存的需求,该filter只缓存页面,不会修改herder的 ETag、Last-Modified、Expires属性
· SimplePageCachingFilterWithBlankPageProblem
当response没有提交时写入缓存,否则不写缓存,该缓存可能导致空白页的错误,需要特别注意!
· SimplePageFragmentCachingFilter
专门针对那些不独立存在,只是被include到其他页面的页面缓存
· SimpleCachingHeadersPageCachingFilter
SimplePageCachingFilter的扩展,会填写herder的 ETag、Last-Modified、Expires属性,可以进一步减少浏览器的访问次数

自定义的PageCachingFilter

以上filter会在filter初始化的时候通过FilterConfig对缓存进行初始化,为了在SpringBoot中方便的通过注解去实例化这些Filter,我们将CacheName的获取做一个定制:

<code>public class CustomPageCachingFilter extends SimpleCachingHeadersPageCachingFilter {
    private final String customCacheName;
    public CustomPageCachingFilter(String name){
        this.customCacheName = name;
    }
    @Override
    protected String getCacheName() {
        return customCacheName;
    }
}
</code>

这样,我们就能很方便的注入cacheName了。
顺便给大家推荐一个Java技术交流群:473984645里面会分享一些资深架构师录制的视频资料:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多!
EhCache只提供了添加缓存的Filter,但是并没有提供删除缓存的Filter,没关系,让我们来自己实现一个:

<code>public class ClearPageCachingFilter implements Filter {
    private final CacheManager cacheManager;
    private final String customCacheName;
    public ClearPageCachingFilter(String name){
        this.customCacheName = name;
        cacheManager = CacheManager.getInstance();
        assert cacheManager != null;
    }
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}
   @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
            ServletException {
        Ehcache ehcache = cacheManager.getEhcache(customCacheName);
        ehcache.removeAll();
    }
    @Override
    public void destroy() {}
}
</code>

现实情况URL的设计是极其复杂的,我们在这里就简单粗暴的将所有cache直接删除,如果缓存设计的比较好,最好可以通过ehcache.remove(key);的方式对cache进行管理。

添加Filter

我们目前使用标注的方式对Filter以及Filter mapping进行管理,目前我们只缓存/blogs/page这一个页面:

<code>@Configuration
@AutoConfigureAfter(CacheConfiguration.class)
public class PageCacheConfiguration {
    @Bean
    public FilterRegistrationBean registerBlogsPageFilter(){
        CustomPageCachingFilter customPageCachingFilter = new CustomPageCachingFilter("com.tmy.mapper.allBlogs");
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(customPageCachingFilter);
        filterRegistrationBean.setUrlPatterns(Lists.newArrayList("/blogs/page"));
        return filterRegistrationBean;
    }
    @Bean
    public FilterRegistrationBean registerClearBlogsPageFilter(){
        ClearPageCachingFilter clearPageCachingFilter = new ClearPageCachingFilter("com.tmy.mapper.allBlogs");
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(clearPageCachingFilter);
        filterRegistrationBean.setUrlPatterns(Lists.newArrayList("/blogs/page/update", "/blogs/page/delete"));
       return filterRegistrationBean;
    }
}
</code>

从以上配置可以看出,我们为/blogs/page注册了一个添加缓存的Filter,/blogs/page请求将被缓存到内存当中。同时,为/blogs/page/update以及/blogs/page/delete注册了清空缓存的Filter,当访问这两个url时,将清空所有的缓存。
顺便给大家推荐一个Java技术交流群:473984645里面会分享一些资深架构师录制的视频资料:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多!

性能对比

访问被缓存的页面

访问http://localhost:8111/blogs/page,刷新,我们可以看到,服务器的响应时间只需要4毫秒,是mysql缓存、数据层缓存、服务层缓存当中最好的。

清空缓存后第一次访问

我们可以将以上几种缓存结合起来一起使用,http://localhost:8111/blogs/page,该请求已经结合了以上三种缓存的实现。因此,当我们访问http://localhost:8111/blogs/page/update清空页面缓存时,再次访问http://localhost:8111/blogs/page也只需要100多毫秒,此时页面缓存没有命

总结

就实践看来,数据层缓存、服务层缓存、页面缓存一层比一层更加高效,但是由于其实现越来越复杂,需要考虑的情况也越来越多,因此,其设计也越来越复杂。
从服务层缓存的实现@CachePut实现来看,在这一层需要我们配置的东西越来越多,已经有很大可能出现数据不一致的现象。而页面缓存的复杂性相对服务层缓存又高了一个层级,因此在针对缓存进行设计的时候,不仅仅考虑缓存所带来的性能提升,还要考虑到更新缓存所带来的性能损失。而且在实践当中,不是数据层缓存、服务层缓存、页面缓存越多越好,需要根据实际情况做出选择。
推荐大家阅读:
Java高级架构学习资料分享+架构师成长之路​
个人整理了更多资料以PDF文件的形式分享给大家,需要查阅的程序员朋友可以来免费领取。还有我的学习笔记PDF文件也免费分享给有需要朋友!
顺便给大家推荐一个Java技术交流群:473984645里面会分享一些资深架构师录制的视频资料:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,063评论 6 510
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,805评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,403评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,110评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,130评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,877评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,533评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,429评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,947评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,078评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,204评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,894评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,546评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,086评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,195评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,519评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,198评论 2 357