nacos统一配置中心源码解析

首先需要引入nacos config jar包

复制代码

<dependency>

    <groupId>com.alibaba.cloud</groupId>

    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>

    <version>2.2.1.RELEASE</version>

</dependency>

复制代码

在nacos控制台提前配置需要的配置文件

配置文件格式支持text、json、xml、yaml、html、properties,注意spring boot启动支持的配置文件格式只能为yaml或properties格式,其它格式的配置文件需要后续我们自己写代码去获取

我们来看db.properties也是就数据库配置

data id就是对应配置文件id,group为分组,配置内容就是properties格式的

再来看bootstrap.properties如何引用这个配置文件

复制代码

spring.application.name=nacos-config

server.port=20200

#命名空间

spring.cloud.nacos.config.namespace=${nacos_register_namingspace:0ca74337-8f42-49c3-aec9-32f268a937c4}

#组名

spring.cloud.nacos.config.group=${spring.application.name}

#文件格式

spring.cloud.nacos.config.file-extension=properties

#nacos server地址

spring.cloud.nacos.config.server-addr=localhost:8848

#加载配置文件

spring.cloud.nacos.config.ext-config[0].data-id=nacos.properties

spring.cloud.nacos.config.ext-config[1].data-id=db.properties

spring.cloud.nacos.config.ext-config[2].data-id=mybatis-plus.properties

复制代码

注意 加载配置文件的分组名默认为DEFAULT_GROUP,如需指定分组 需要再指定

复制代码

spring.cloud.nacos.config.ext-config[0].data-id=nacos.properties

spring.cloud.nacos.config.ext-config[0].group=${spring.cloud.nacos.config.group}

#或者

spring.cloud.nacos.config.ext-config[1].data-id=undertow.properties

spring.cloud.nacos.config.ext-config[1].group=MY_DEFAULT

复制代码

在这里解释下namespace和group的概念,namespace可以用来解决不同环境的问题,group是来管理配置分组的,它们的关系如下图

spring boot启动容器如何加载nacos config配置文件

这个配置作用是spring在启动之间准备上下文时会启用这个配置 来导入nacos相关配置文件,为后续容器启动做准备

来看NacosConfigBootstrapConfiguration这个配置类

NacosConfigProperties:对应我们上面在bootstrap.properties中对应的配置信息

NacosConfigManager: 持有NacosConfigProperties和ConfigService,ConfigService用来查询 发布配置的相关接口

NacosPropertySourceLocator:它实现了PropertySourceLocator ,spring boot启动时调用PropertySourceLocator.locate(env)用来加载配置信息,下面来看相关源码

复制代码

/******************************************NacosPropertySourceLocator******************************************/

public PropertySource<?> locate(Environment env) {

    ConfigService configService = this.nacosConfigProperties.configServiceInstance();

    if (null == configService) {

        log.warn("no instance of config service found, can't load config from nacos");

        return null;

    } else {

        long timeout = (long)this.nacosConfigProperties.getTimeout();

        this.nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService, timeout);

        String name = this.nacosConfigProperties.getName();

        String dataIdPrefix = this.nacosConfigProperties.getPrefix();

        if (StringUtils.isEmpty(dataIdPrefix)) {

            dataIdPrefix = name;

        }

        if (StringUtils.isEmpty(dataIdPrefix)) {

            dataIdPrefix = env.getProperty("spring.application.name");

        }

        CompositePropertySource composite = new CompositePropertySource("NACOS");

        // 加载共享的配置文件 不同指定分组 默认DEFAULT_GROUP,对应配置spring.cloud.nacos.config.sharedDataids=shared_1.properties,shared_2.properties

        this.loadSharedConfiguration(composite);

        // 对应spring.cloud.nacos.config.ext-config[0].data-id=nacos.properties的配置

        this.loadExtConfiguration(composite);

        // 加载当前应用配置

        this.loadApplicationConfiguration(composite, dataIdPrefix, this.nacosConfigProperties, env);

        return composite;

    }

}

// 看一个加载实现即可 流程都差不多 具体实现在NacosPropertySourceBuilder.loadNacosData()方法完成

/******************************************具体实现在NacosPropertySourceBuilder******************************************/

private Properties loadNacosData(String dataId, String group, String fileExtension) {

        String data = null;

        try {

            // 向nacos server拉取配置文件

            data = this.configService.getConfig(dataId, group, this.timeout);

            if (!StringUtils.isEmpty(data)) {

                log.info(String.format("Loading nacos data, dataId: '%s', group: '%s'", dataId, group));

                // spring boot配置当然只支持properties和yaml文件格式

                if (fileExtension.equalsIgnoreCase("properties")) {

                    Properties properties = new Properties();

                    properties.load(new StringReader(data));

                    return properties;

                }

                if (fileExtension.equalsIgnoreCase("yaml") || fileExtension.equalsIgnoreCase("yml")) {

                    YamlPropertiesFactoryBean yamlFactory = new YamlPropertiesFactoryBean();

                    yamlFactory.setResources(new Resource[]{new ByteArrayResource(data.getBytes())});

                    return yamlFactory.getObject();

                }

            }

        } catch (NacosException var6) {

            log.error("get data from Nacos error,dataId:{}, ", dataId, var6);

        } catch (Exception var7) {

            log.error("parse data from Nacos error,dataId:{},data:{},", new Object[]{dataId, data, var7});

        }

        return EMPTY_PROPERTIES;

    }

复制代码

至此我们在nacos上配置的properties和yaml文件都载入到spring配置文件中来了,后面可通过context.Environment.getProperty(propertyName)来获取相关配置信息

配置如何随spring boot加载进来我们说完了,接来下来看修改完配置后如何实时刷新

nacos config动态刷新

当nacos config更新后,根据配置中的refresh属性来判断是否刷新配置,配置如下

spring.cloud.nacos.config.ext-config[0].refresh=true

首先sprin.factories 配置了EnableAutoConfiguration=NacosConfigAutoConfiguration,NacosConfigAutoConfiguration配置类会注入一个NacosContextRefresher,它首先监听了ApplicationReadyEvent,然后注册一个nacos listener用来监听nacos config配置修改后发布一个spring refreshEvent用来刷新配置和应用

复制代码

public class NacosContextRefresher implements ApplicationListener<ApplicationReadyEvent>, ApplicationContextAware

public void onApplicationEvent(ApplicationReadyEvent event) {

    // 只注册一次

    if (this.ready.compareAndSet(false, true)) {

        this.registerNacosListenersForApplications();

    }

}


private void registerNacosListenersForApplications() {

    if (this.refreshProperties.isEnabled()) {

        Iterator var1 = NacosPropertySourceRepository.getAll().iterator();

        while(var1.hasNext()) {

            NacosPropertySource nacosPropertySource = (NacosPropertySource)var1.next();

            // 对应刚才所说的配置 需要配置文件是否需要刷新

            if (nacosPropertySource.isRefreshable()) {

                String dataId = nacosPropertySource.getDataId();

                // 注册nacos监听器

                this.registerNacosListener(nacosPropertySource.getGroup(), dataId);

            }

        }

    }

}


private void registerNacosListener(final String group, final String dataId) {

    Listener listener = (Listener)this.listenerMap.computeIfAbsent(dataId, (i) -> {

        return new Listener() {

            public void receiveConfigInfo(String configInfo) {

                NacosContextRefresher.refreshCountIncrement();

                String md5 = "";

                if (!StringUtils.isEmpty(configInfo)) {

                    try {

                        MessageDigest md = MessageDigest.getInstance("MD5");

                        md5 = (new BigInteger(1, md.digest(configInfo.getBytes("UTF-8")))).toString(16);

                    } catch (UnsupportedEncodingException | NoSuchAlgorithmException var4) {

                        NacosContextRefresher.log.warn("[Nacos] unable to get md5 for dataId: " + dataId, var4);

                    }

                }

                // 添加刷新记录

                NacosContextRefresher.this.refreshHistory.add(dataId, md5);

                // 发布一个spring refreshEvent事件 对应监听器为RefreshEventListener 该监听器会完成配置的更新应用

                NacosContextRefresher.this.applicationContext.publishEvent(new RefreshEvent(this, (Object)null, "Refresh Nacos config"));

                if (NacosContextRefresher.log.isDebugEnabled()) {

                    NacosContextRefresher.log.debug("Refresh Nacos config group " + group + ",dataId" + dataId);

                }

            }

            public Executor getExecutor() {

                return null;

            }

        };

    });

    try {

        this.configService.addListener(dataId, group, listener);

    } catch (NacosException var5) {

        var5.printStackTrace();

    }

}

复制代码

我们说完了nacos config动态刷新,那么肯定有对应的动态监听,nacos config会监听nacos server上配置的更新状态

nacos config动态监听

一般来说客户端和服务端数据交互无非就两种方式

pull:客户端主动从服务器拉取数据

push: 由服务端主动向客户端推送数据

这两种模式优缺点各不一样,pull模式需要考虑的是什么时候向服务端拉取数据 可能会存在数据延迟问题,而push模式需要客户端和服务端维护一个长连接 如果客户端较多会给服务端造成压力 但它的实时性会更好

nacos采用的是pull模式,但它作了优化 可以看做是pull+push,客户端会轮询向服务端发出一个长连接请求,这个长连接最多30s就会超时,服务端收到客户端的请求会先判断当前是否有配置更新,有则立即返回

如果没有服务端会将这个请求拿住“hold”29.5s加入队列,最后0.5s再检测配置文件无论有没有更新都进行正常返回,但等待的29.5s期间有配置更新可以提前结束并返回,下面会在源码中讲解具体怎么处理的

nacos client处理

动态监听的发起是在ConfigService的实现类NacosConfigService的构造方法中,它是对外nacos config api接口,在之前加载配置文件和NacosContextRefresher构造方法中都会获取或创建

这里都会先判断是否已经创建了ConfigServer,没有则实例化一个NacosConfigService,来看它的构造函数

复制代码

/***************************************** NacosConfigService *****************************************/

public NacosConfigService(Properties properties) throws NacosException {

    String encodeTmp = properties.getProperty(PropertyKeyConst.ENCODE);

    if (StringUtils.isBlank(encodeTmp)) {

        encode = Constants.ENCODE;

    } else {

        encode = encodeTmp.trim();

    }

    initNamespace(properties);

    // 用来向nacos server发起请求的代理,这里用到了装饰模式

    agent = new MetricsHttpAgent(new ServerHttpAgent(properties));

    agent.start();

    // 客户端的一个工作类,agent作为它的构造传参 可猜想到里面肯定会做一些远程调用

    worker = new ClientWorker(agent, configFilterChainManager, properties);

}

/***************************************** ClientWorker *****************************************/

public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager, final Properties properties) {

    this.agent = agent;

    this.configFilterChainManager = configFilterChainManager;

    // Initialize the timeout parameter

    init(properties);

    // 这个线程池只有一个核心线程 用来执行checkConfigInfo()方法

    executor = Executors.newScheduledThreadPool(1, new ThreadFactory() {

        @Override

        public Thread newThread(Runnable r) {

            Thread t = new Thread(r);

            t.setName("com.alibaba.nacos.client.Worker." + agent.getName());

            t.setDaemon(true);

            return t;

        }

    });

    // 其它需要执行线程的地方都交给这个线程池来处理

    executorService = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactory() {

        @Override

        public Thread newThread(Runnable r) {

            Thread t = new Thread(r);

            t.setName("com.alibaba.nacos.client.Worker.longPolling." + agent.getName());

            t.setDaemon(true);

            return t;

        }

    });


    // 执行一个调用checkConfigInfo()方法的周期性任务,每10ms执行一次,首次执行延迟1ms后执行

    executor.scheduleWithFixedDelay(new Runnable() {

        @Override

        public void run() {

            try {

                checkConfigInfo();

            } catch (Throwable e) {

                LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", e);

            }

        }

    }, 1L, 10L, TimeUnit.MILLISECONDS);

}

复制代码

NacosConfigService构造方法主要创建一个agent 它是用来向nacos server发出请求的,然后又创建了一个clientwoker,它的构造方法创建了两个线程池,第一个线程池只有一个核心线程,它会执行一个周期性任务只用来调用checkconfiginfo()方法,第二个线程是后续由需要执行线程的地方都交给它来执行,下面重点来看checkconfiginfo()方法

复制代码

public void checkConfigInfo() {

    // 分任务

    int listenerSize = cacheMap.get().size();

    // 向上取整为批数

    int longingTaskCount = (int) Math.ceil(listenerSize / ParamUtil.getPerTaskConfigSize());

    if (longingTaskCount > currentLongingTaskCount) {

        for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) {

            executorService.execute(new LongPollingRunnable(i));

        }

        currentLongingTaskCount = longingTaskCount;

    }

}

AtomicReference<Map<String, CacheData>> cacheMap = new AtomicReference<Map<String, CacheData>>(

    new HashMap<String, CacheData>());

复制代码

cacheMap:缓存着需要刷新的配置,它是在调用ConfigService 添加监听器方式时会放入,可以自定义监听配置刷新

复制代码

// 添加一个config监听器,用来监听dataId为ErrorCode,group为DEFAULT_GROUP的config

configService.addListener("ErrorCode","DEFAULT_GROUP",new Listener() {

    @Override

    public Executor getExecutor() {

        return null;

    }

    @Override

    public void receiveConfigInfo(String s) { //当配置更新时会调用监听器该方法

        Map<String, Map<String, String>> map = JSON.parseObject(s, Map.class);

        // 根据自己的业务需要来处理

    }

});

复制代码

这里采用了一个策略:将cacheMap中的数量以3000分一个组,分别创建一个LongPollingRunnable用来监听配置更新,这个LongPollingRunnable就是我们之前所说的长连接任务,来看这个长连接任务

复制代码

class LongPollingRunnable implements Runnable {

    private int taskId;

    public LongPollingRunnable(int taskId) {

        this.taskId = taskId;

    }

    @Override

    public void run() {

        List<CacheData> cacheDatas = new ArrayList<CacheData>();

        List<String> inInitializingCacheList = new ArrayList<String>();

        try {

            // check failover config

            for (CacheData cacheData : cacheMap.get().values()) {

                if (cacheData.getTaskId() == taskId) {

                    cacheDatas.add(cacheData);

                    try {

                        // 1、检查本地配置

                        checkLocalConfig(cacheData);

                        if (cacheData.isUseLocalConfigInfo()) {

                            cacheData.checkListenerMd5();

                        }

                    } catch (Exception e) {

                        LOGGER.error("get local config info error", e);

                    }

                }

            }

            // 2、向nacos server发出一个长连接 30s超时,返回nacos server有更新过的dataIds

            List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);

            LOGGER.info("get changedGroupKeys:" + changedGroupKeys);

            for (String groupKey : changedGroupKeys) {

                String[] key = GroupKey.parseKey(groupKey);

                String dataId = key[0];

                String group = key[1];

                String tenant = null;

                if (key.length == 3) {

                    tenant = key[2];

                }

                try {

                    // 3、向nacos server请求获取config最新内容

                    String[] ct = getServerConfig(dataId, group, tenant, 3000L);

                    CacheData cache = cacheMap.get().get(GroupKey.getKeyTenant(dataId, group, tenant));

                    cache.setContent(ct[0]);

                    if (null != ct[1]) {

                        cache.setType(ct[1]);

                    }

                }

            }

            // 4、对有变化的config调用对应监听器去处理

            for (CacheData cacheData : cacheDatas) {

                if (!cacheData.isInitializing() || inInitializingCacheList

                    .contains(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant))) {

                    cacheData.checkListenerMd5();

                    cacheData.setInitializing(false);

                }

            }

            inInitializingCacheList.clear();

            // 继续轮询

            executorService.execute(this);

        } catch (Throwable e) {

            // 发生异常延迟执行

            executorService.schedule(this, taskPenaltyTime, TimeUnit.MILLISECONDS);

        }

    }

}

复制代码

这个长轮询主要做了4个步骤

检查本地配置,如果存在本地配置,并且与缓存中的本地配置版本不一样,把本地配置内容更新到缓存,并触发事件,这块源码比较简单,读者跟到源码一读编制

向nacos server发出一个长连接,30s超时,nacos server会返回有变化的dataIds

根据变化的dataId,从服务端拉取最新的配置内容然后更新到缓存中

对有变化的配置 触发事件监听器来处理

讲完了nacos client处理流程,再来看服务端这边怎么处理这个长连接的

nacos server处理

服务端长连接接口是/config/listener,对应源码包为config

复制代码

/****************************************** ConfigController ******************************************/

@PostMapping("/listener")

@Secured(action = ActionTypes.READ, parser = ConfigResourceParser.class)

public void listener(HttpServletRequest request, HttpServletResponse response)

        throws ServletException, IOException {

    request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", true);

    String probeModify = request.getParameter("Listening-Configs");

    if (StringUtils.isBlank(probeModify)) {

        throw new IllegalArgumentException("invalid probeModify");

    }


    probeModify = URLDecoder.decode(probeModify, Constants.ENCODE);

    // 需要检查更新的config信息

    Map<String, String> clientMd5Map;

    try {

        clientMd5Map = MD5Util.getClientMd5Map(probeModify);

    } catch (Throwable e) {

        throw new IllegalArgumentException("invalid probeModify");

    }


    // 长连接处理

    inner.doPollingConfig(request, response, clientMd5Map, probeModify.length());

}

/****************************************** ConfigServletInner ******************************************/

public String doPollingConfig(HttpServletRequest request, HttpServletResponse response,

            Map<String, String> clientMd5Map, int probeRequestSize) throws IOException {


    // 判断是否支持长轮询

    if (LongPollingService.isSupportLongPolling(request)) {

        // 长轮询处理

        longPollingService.addLongPollingClient(request, response, clientMd5Map, probeRequestSize);

        return HttpServletResponse.SC_OK + "";

    }


    // 不支持长轮询,直接与当前配置作比较,返回有变更的配置

    List<String> changedGroups = MD5Util.compareMd5(request, response, clientMd5Map);


    // Compatible with short polling result.

    String oldResult = MD5Util.compareMd5OldResult(changedGroups);

    String newResult = MD5Util.compareMd5ResultString(changedGroups);


    /*

    * 省略

    * 会响应变更的配置信息

    */

    return HttpServletResponse.SC_OK + "";

}

/****************************************** LongPollingService ******************************************/

public void addLongPollingClient(HttpServletRequest req, HttpServletResponse rsp, Map<String, String> clientMd5Map,

            int probeRequestSize) {


    String str = req.getHeader(LongPollingService.LONG_POLLING_HEADER);

    String noHangUpFlag = req.getHeader(LongPollingService.LONG_POLLING_NO_HANG_UP_HEADER);

    String appName = req.getHeader(RequestUtil.CLIENT_APPNAME_HEADER);

    String tag = req.getHeader("Vipserver-Tag");


    // 服务端这边最多处理时长29.5s,需要留0.5s来返回,以免客户端那边超时

    int delayTime = SwitchService.getSwitchInteger(SwitchService.FIXED_DELAY_TIME, 500);

    // Add delay time for LoadBalance, and one response is returned 500 ms in advance to avoid client timeout.

    long timeout = Math.max(10000, Long.parseLong(str) - delayTime);

    if (isFixedPolling()) {

        timeout = Math.max(10000, getFixedPollingInterval());

        // Do nothing but set fix polling timeout.

    } else {

        // 不支持长轮询 本地对比返回

        long start = System.currentTimeMillis();

        List<String> changedGroups = MD5Util.compareMd5(req, rsp, clientMd5Map);

        if (changedGroups.size() > 0) {

            generateResponse(req, rsp, changedGroups);

            // log....

            return;

        } else if (noHangUpFlag != null && noHangUpFlag.equalsIgnoreCase(TRUE_STR)) {

            // log....

            return;

        }

    }

    String ip = RequestUtil.getRemoteIp(req);


    // 将http响应交给异步线程,返回一个异步响应上下文, 当配置更新后可以主动调用及时返回,不用非等待29.5s

    final AsyncContext asyncContext = req.startAsync();


    // AsyncContext.setTimeout() is incorrect, Control by oneself

    asyncContext.setTimeout(0L);

    // 执行客户端长连接任务,

    ConfigExecutor.executeLongPolling(

            new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag));

}

/****************************************** ClientLongPolling ******************************************/

class ClientLongPolling implements Runnable {


    @Override

    public void run() {

        // 提交一个任务,延迟29.5s执行

        asyncTimeoutFuture = ConfigExecutor.scheduleLongPolling(new Runnable() {

            @Override

            public void run() {

                try {

                    getRetainIps().put(ClientLongPolling.this.ip, System.currentTimeMillis());


                    // Delete subsciber's relations.

                    allSubs.remove(ClientLongPolling.this);


                    if (isFixedPolling()) {

                        // 检查变更配置 并相应

                        List<String> changedGroups = MD5Util

                                .compareMd5((HttpServletRequest) asyncContext.getRequest(),

                                        (HttpServletResponse) asyncContext.getResponse(), clientMd5Map);

                        if (changedGroups.size() > 0) {

                            sendResponse(changedGroups);

                        } else {

                            sendResponse(null);

                        }

                    } else {

                        sendResponse(null);

                    }

                } catch (Throwable t) {

                    LogUtil.DEFAULT_LOG.error("long polling error:" + t.getMessage(), t.getCause());

                }


            }


        }, timeoutTime, TimeUnit.MILLISECONDS);


        allSubs.add(this);

    }

}

final Queue<ClientLongPolling> allSubs

复制代码

上面大部分地方都比较好懂,主要解释下ClientLongPolling作用,它首先会提交一个任务,无论配置有没有更新 最终都会进行响应,延迟29.5s执行,然后会把自己添加到一个队列中,之前说过,服务端这边配置有更新后 会找出正在等待配置更新的长连接任务,提前结束这个任务并返回,

来看这一步是怎么处理的

复制代码

public LongPollingService() {

    allSubs = new ConcurrentLinkedQueue<ClientLongPolling>();


    ConfigExecutor.scheduleLongPolling(new StatTask(), 0L, 10L, TimeUnit.SECONDS);


    // Register LocalDataChangeEvent to NotifyCenter.

    NotifyCenter.registerToPublisher(LocalDataChangeEvent.class, NotifyCenter.ringBufferSize);


    // Register A Subscriber to subscribe LocalDataChangeEvent.

    NotifyCenter.registerSubscriber(new Subscriber() {


        @Override

        public void onEvent(Event event) {

            if (isFixedPolling()) {

                // Ignore.

            } else {

                if (event instanceof LocalDataChangeEvent) {

                    LocalDataChangeEvent evt = (LocalDataChangeEvent) event;

                    ConfigExecutor.executeLongPolling(new DataChangeTask(evt.groupKey, evt.isBeta, evt.betaIps));

                }

            }

        }


        @Override

        public Class<? extends Event> subscribeType() {

            return LocalDataChangeEvent.class;

        }

    });


}

class DataChangeTask implements Runnable {


    @Override

    public void run() {

        try {

            ConfigCacheService.getContentBetaMd5(groupKey);

            // 找出等在该配置的长连接,然后进行提前返回

            for (Iterator<ClientLongPolling> iter = allSubs.iterator(); iter.hasNext(); ) {

                ClientLongPolling clientSub = iter.next();

                if (clientSub.clientMd5Map.containsKey(groupKey)) {

                    // If published tag is not in the beta list, then it skipped.

                    if (isBeta && !CollectionUtils.contains(betaIps, clientSub.ip)) {

                        continue;

                    }


                    // If published tag is not in the tag list, then it skipped.

                    if (StringUtils.isNotBlank(tag) && !tag.equals(clientSub.tag)) {

                        continue;

                    }


                    getRetainIps().put(clientSub.ip, System.currentTimeMillis());

                    iter.remove(); // Delete subscribers' relationships.

                    clientSub.sendResponse(Arrays.asList(groupKey));

                }

            }

        } catch (Throwable t) {

            LogUtil.DEFAULT_LOG.error("data change error: {}", ExceptionUtil.getStackTrace(t));

        }

    }

}

亚马逊测评 www.yisuping.cn

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

推荐阅读更多精彩内容