面试那些事(一)2018-07-05

一、上海某雷科技公司

刚到公司,办公室只有一个人,那就是人事。这里的环境也不是很好,貌似窗台还在漏水。前天晚上通知说要带电脑,说有道题要做。果真,在会议室就开始让我做题。

1、设计一个程序可以实现从xml和csv文件中读取数据并通过整合后写出到一个TXT文件中。

现有2个数据文件,Apple.xml、Orange.csv。
Apple.xml中存放了, 客户的苹果持有数量,数据以xml格式存放。
Orange.csv中存放了,客户的桔子持有数量,数据以csv格式存放。

请用Java实现以下功能:
1.读取2个文件的中的信息,如果存在相同CustomerCode,则合并成同一条客户记录(类似数据库查询的Full Join规则)。记录格式参见“结果样例.txt”。
2.记录列表按单个客户所拥有的水果总数降序排列。
3.将最终记录列表,输出保存成一个文本文件。

PS:可以百度

  • Apple.xml
<?xml version="1.0" encoding="UTF-8"?>
<Customers>
    <Customer>
        <CustomerCode>C001</CustomerCode>
        <CustomerName 苹果数量="10">张三</CustomerName>       
    </Customer>
    <Customer>
        <CustomerCode>C002</CustomerCode>
        <CustomerName 苹果数量="20">李四</CustomerName>       
    </Customer>
    <Customer>
        <CustomerCode>C003</CustomerCode>
        <CustomerName 苹果数量="30">王五</CustomerName>       
    </Customer>
</Customers>
  • Orang.csv

    CustomerCode,桔子数量
    C010,100
    C001,100
    C909,888
    C003,5
    C002,20
    
  • 结果样例.txt

    CustomerCode|CustomerName|苹果数量|桔子数量|水果总数
    C909|null|0|888|888
    C001|张三|10|100|110
    ...
    

我的思路是:

​ S1、先解析Apple.xml文件,获取一个Map<String,Customer>集合;

​ S2、再解析Orang.csv并合并数据,返回一个信息比较齐全的Map;

​ S3、将map转为list,使用Collections的排序方法排序;

​ S4、将list打印到再写入到txt中。

表示在写的过程中,我忘了排序了。🤢

想法很美好,但是由于几乎没有写过解析xml的文件的代码,所以百度了很多例子,还测试了测试,浪费了很多时间。😥

  • Customer类
public class Customer {
    private String CustomerCode;
    private String CustomerName;
    private Integer AppleNum = 0;
    private Integer OrangeNum = 0;

    public String getCustomerCode() {
        return CustomerCode;
    }

    public void setCustomerCode(String customerCode) {
        CustomerCode = customerCode;
    }

    public String getCustomerName() {
        return CustomerName;
    }

    public void setCustomerName(String customerName) {
        CustomerName = customerName;
    }

    public Integer getAppleNum() {
        return AppleNum;
    }

    public void setAppleNum(Integer appleNum) {
        AppleNum = appleNum;
    }

    public Integer getOrangeNum() {
        return OrangeNum;
    }

    public void setOrangeNum(Integer orangeNum) {
        OrangeNum = orangeNum;
    }

    @Override
    public String toString() {
        return CustomerCode+"|"+CustomerName+"|"+AppleNum+"|"+OrangeNum+"|"+(AppleNum+OrangeNum);
    }

    public Customer(String customerCode, String customerName, Integer appleNum, Integer orangeNum) {
        CustomerCode = customerCode;
        CustomerName = customerName;
        AppleNum = appleNum;
        OrangeNum = orangeNum;
    }

    public Customer() {
    }
}
  • Main
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.io.*;
import java.util.*;

public class Main {

    public static void main(String[] args) {

        Map<String, Customer> xmlmap = xml2Map();
        Map<String, Customer> withcsvmap = readCsv(xmlmap);
        List<Map.Entry<String, Customer>> list = mapSort(withcsvmap);
        toText(list);
        
        System.out.println("finish.....");
    }
    
    //4、输出
    private static void toText(List<Map.Entry<String, Customer>> list) {
        try {
            File file = new File("final.txt");
            PrintStream ps = new PrintStream(new FileOutputStream(file));
            ps.println("CustomerCode|CustomerName|苹果数量|桔子数量|水果总数");
            for (Map.Entry<String, Customer> entry : list) {
                ps.println(entry.getValue().toString());
            }

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
    
    //3、list排序
    private static List<Map.Entry<String, Customer>> mapSort(Map<String, Customer> withcsvmap) {
        Set<Map.Entry<String, Customer>> entries = withcsvmap.entrySet();
        List<Map.Entry<String, Customer>> list 
            = new ArrayList<Map.Entry<String, Customer>>(entries);
        Collections.sort(list, new Comparator<Map.Entry<String, Customer>>() {
            public int compare(Map.Entry<String, Customer> o1, Map.Entry<String, Customer> o2) {
                return  (o2.getValue().getAppleNum() + o2.getValue().getOrangeNum())
                        -(o1.getValue().getAppleNum() + o1.getValue().getOrangeNum()) ;
            }
        });

        return list;
    }
    //2、数据合并
    private static Map<String, Customer> readCsv(Map<String, Customer> customers) {
        try {
            BufferedReader reader = new BufferedReader(new FileReader("Orange.csv"));
            reader.readLine();//第一行信息,为标题信息,不用,如果需要,注释掉
            String line ;
            while ((line = reader.readLine()) != null) {
                String item[] = line.split(",");//CSV格式文件为逗号分隔符文件,这里根据逗号切分
                String customerCode = item[0];
                String orangeNum = item[1];
                if (customers.containsKey(customerCode)) {
                    customers.get(customerCode).setOrangeNum(Integer.parseInt(orangeNum));
                } else {
                    customers.put(customerCode, 
                               new Customer(customerCode,null,0,Integer.parseInt(orangeNum)));
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return customers;
    }
    //1、读取xml
    private static Map<String, Customer> xml2Map() {
        String parseStr;

        Map<String, Customer> customers = new HashMap<String, Customer>();
        try {
            SAXReader sax = new SAXReader();//创建一个SAXReader对象
            File xmlFile = new File("Apple.xml");//根据指定的路径创建file对象
            Document document = sax.read(xmlFile);//获取document对象,如果文档无节点,则会抛出Exception提前结束; 
            Element roots = document.getRootElement();
            parseStr = roots.getText();
            //只有根节点……
            Iterator elements = roots.elementIterator();
            while (elements.hasNext()) {
                Element child = (Element) elements.next();
                List subElemets = child.elements();
                Customer customer = new Customer();
                for (Object o:subElemets) {
                    Element subChild = (Element) o;
                    if (subChild.getName().equals("CustomerCode")) {
                        customer.setCustomerCode(subChild.getText());
                    }
                    if (subChild.getName().equals("CustomerName")) {
                        customer.setCustomerName(subChild.getText());
                        List<Attribute> listAttr = subChild.attributes();
                        customer.setAppleNum(Integer.parseInt(listAttr.get(0).getValue()));
                    }
                    customers.put(customer.getCustomerCode(), customer);
                }
            }
        } catch (DocumentException e) {
            e.printStackTrace();
        }
        return customers;
    }
}

下午回来在网上百度了一下一模一样的题啊,我怎么就没有想到直接百度原题目呢。。

  • com.zhangpn.main.MainRun
package com.zhangpn.main;

import com.zhangpn.service.ProcessList;
import com.zhangpn.utils.ReadFileToList;

public class MainRun {

    public static void main(String[] args) {

        ProcessList.PrintList(ProcessList.JoinFull(ReadFileToList.XmlDataRead("src/Apple.xml"),
                ReadFileToList.CsvDataRead("src/Orange.csv")),"src/text.txt");
    }

}

  • com.zhangpn.service.ProcessList;
package com.zhangpn.service;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import com.zhangpn.comparator.ComparatorCustomer;
import com.zhangpn.entity.Customer;

public class ProcessList {
    /**
     * 将结果进行融合
     * @param AppleCustomersList
     * @param OrangeCustomersList
     * @return
     */
    public static List<Customer> JoinFull(List<Customer> AppleCustomersList,List<Customer> OrangeCustomersList){
        //最终的返回结果,等待被构造
        List<Customer> ResultList = new ArrayList<>();
        
        // 记录重复元素,便于找出Orangecustomer剩余元素
        List<String> RepeatedCodes = new ArrayList<>();
        
        // 遍历Applecustomer,将重复信息重整,再遍历Orangecustomer,将剩余元素添加进来
        for (Customer Applecustomer : AppleCustomersList) {
            // 默认是没有重复Code
            boolean flag = false;
            // 准备存储的客户信息对象[先遍历存储Applecustomer]
            Customer ApplecustomerTemp = new Customer();
            for (Customer Orangecustomer : OrangeCustomersList) {
                // 有重复Code,说明是同一客户,数据重新整合
                if(Applecustomer.getCustomerCode().equals(Orangecustomer.getCustomerCode())){
                    
                    // 标记改为true,说明有重复值
                    flag = true;
                    
                    // 重新组合了客户信息
                    ApplecustomerTemp.setCustomerCode(Applecustomer.getCustomerCode());
                    ApplecustomerTemp.setCustomerName(Applecustomer.getCustomerName());
                    ApplecustomerTemp.setAppleNum(Applecustomer.getAppleNum());
                    ApplecustomerTemp.setOrangeNum(Orangecustomer.getOrangeNum());
                    ApplecustomerTemp.setAllNum(Applecustomer.getAppleNum() + Orangecustomer.getOrangeNum());
                    
                    //将重复Code记录下来
                    String RepeatedCode = new String(Applecustomer.getCustomerCode());
                    RepeatedCodes.add(RepeatedCode);
                    
                    break;
                }
            }
            
            // 如果没有重复对象将原来数据复制保存
            if(!flag){
                ApplecustomerTemp.setCustomerCode(Applecustomer.getCustomerCode());
                ApplecustomerTemp.setCustomerName(Applecustomer.getCustomerName());
                ApplecustomerTemp.setAppleNum(Applecustomer.getAppleNum());
                ApplecustomerTemp.setAllNum(Applecustomer.getAppleNum());
            }
            
            // 增加到ResultList
            ResultList.add(ApplecustomerTemp);
        }
        
        // 遍历Orangecustomer
        for (Customer Orangecustomer : OrangeCustomersList) {
            // 标记是否属于重复元素,默认是否
            boolean flag = false;
            for (String RepeatedCode : RepeatedCodes) {
                if(RepeatedCode.equals(Orangecustomer.getCustomerCode())){
                    flag = true;
                }
            }
            
            // 如果不重复,那么添加到ResultList
            if(!flag){
                Customer OrangecustomerTemp = new Customer();
                OrangecustomerTemp.setCustomerCode(Orangecustomer.getCustomerCode());
                OrangecustomerTemp.setCustomerName(Orangecustomer.getCustomerName());
                OrangecustomerTemp.setAppleNum(Orangecustomer.getAppleNum());
                OrangecustomerTemp.setOrangeNum(Orangecustomer.getOrangeNum());
                OrangecustomerTemp.setAllNum(Orangecustomer.getAppleNum() + Orangecustomer.getOrangeNum());
                ResultList.add(OrangecustomerTemp);
            }
        }
        
        return ResultList;
    }
    
    /**
     * 对结果进行降序排序
     * @param CustomersList
     */
    public static void SortList(List<Customer> CustomersList){
        ComparatorCustomer comparatorCustomer=new ComparatorCustomer();
        Collections.sort(CustomersList,comparatorCustomer);
    }
    
    /**
     * 对集合进行打印
     * @param CustomersList
     */
    public static void PrintList(List<Customer> CustomersList,String url){
        FileOutputStream fs;
        try {
            fs = new FileOutputStream(new File(url));
            PrintStream p = new PrintStream(fs);
            p.println("CustomerCode|CustomerName|苹果数量|桔子数量|水果总数");
            
            //降序排列
            SortList(CustomersList);
            
            for (Customer Customer : CustomersList) {
                String buffer = Customer.getCustomerCode() + "|" + Customer.getCustomerName() + "|"
                + Customer.getAppleNum() + "|" + Customer.getOrangeNum() + "|" + Customer.getAllNum();
                p.println(buffer);              
            }
            p.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
    
}
  • com.zhangpn.comparator.ComparatorCustomer
package com.zhangpn.comparator;

import java.util.Comparator;

import com.zhangpn.entity.Customer;

/**
 * 作用:用于List排序
 * @author Administrator
 *
 */
public class ComparatorCustomer implements Comparator {

    @Override
    public int compare(Object o1, Object o2) {
        Customer customer1 = (Customer) o1;
        Customer customer2 = (Customer) o2;

        // 根据水果总数进行排序
        int flag = 1;
        if (customer1.getAllNum() > customer2.getAllNum())
            flag = -1;
        return flag;
    }

}
  • com.zhangpn.entity.Customer
package com.zhangpn.entity;

/**
 * Customer实体类
 * @author Administrator
 *
 */
public class Customer {
    private String CustomerCode;
    private String CustomerName;
    private int AppleNum;
    private int OrangeNum;
    private int AllNum;

    public String getCustomerCode() {
        return CustomerCode;
    }

    public void setCustomerCode(String customerCode) {
        CustomerCode = customerCode;
    }

    public String getCustomerName() {
        return CustomerName;
    }

    public void setCustomerName(String customerName) {
        CustomerName = customerName;
    }

    public int getAppleNum() {
        return AppleNum;
    }

    public void setAppleNum(int appleNum) {
        AppleNum = appleNum;
    }

    public int getOrangeNum() {
        return OrangeNum;
    }

    public void setOrangeNum(int orangeNum) {
        OrangeNum = orangeNum;
    }

    public int getAllNum() {
        return AllNum;
    }

    public void setAllNum(int allNum) {
        AllNum = allNum;
    }

    @Override
    public String toString() {
        return "Customer [CustomerCode=" + CustomerCode + ", CustomerName=" + CustomerName + ", AppleNum=" + AppleNum
                + ", OrangeNum=" + OrangeNum + ", AllNum=" + AllNum + "]";
    }

}
  • com.zhangpn.util.ReadFileToList
package com.zhangpn.utils;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import com.zhangpn.entity.Customer;

public class ReadFileToList {
    /**
     * 读取Xml文件数据
     * @param url
     * @return
     */
    public static List<Customer> XmlDataRead(String url) {
        // 将读取数据封装到List<Customer>集合中
        List<Customer> customersList = new ArrayList<>();

        // 构建一个读取器
        SAXReader reader = new SAXReader();
        
        // 定义文档类引用
        Document document;
        
        try {
            // 读取XML文件
            document = reader.read(url);
            
            // 根节点Customers
            Element Customers = document.getRootElement();
            
            // 遍历根节点Customers的子节点
            for (Iterator<Element> it1 = Customers.elementIterator(); it1.hasNext();) {
                
                // 依次取出子节点Customer
                Element Customer = it1.next();
                
                // 子节点的子节点[取出内容]
                Element customerCode = Customer.element("CustomerCode");
                
                // 子节点的子节点[取出内容]
                Element customerName = Customer.element("CustomerName");
                
                // 子节点的子节点[取出属性值]
                Attribute appleNum = customerName.attribute("苹果数量");
                
                // 构建一个客户信息类,赋上属性值
                Customer customer = new Customer();
                customer.setCustomerCode(customerCode.getText());
                customer.setCustomerName(customerName.getText());
                customer.setAppleNum(Integer.parseInt(appleNum.getValue()));
                customer.setAllNum(customer.getAppleNum());
                
                // 添加到CustomersList集合
                customersList.add(customer);
            }

        } catch (DocumentException e) {
            e.printStackTrace();
        }
        
        // 返回集合结果集
        return customersList;
    }

    /**
     * 读取Csv文件数据
     * @param url
     * @return
     */
    public static List<Customer> CsvDataRead(String url) {
        // 将读取数据封装到List<Customer>集合中
        List<Customer> customersList = new ArrayList<>();

        // 加载文件,加载到输入流中
        File csv = new File(url);
        BufferedReader br = null;
        try {
            br = new BufferedReader(new FileReader(csv));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        
        // 每次读取到临时变量
        String line = "";
        
        // 记录第一行数据,排除不用
        int count = 1;
        try {
            while ((line = br.readLine()) != null) {
                
                // count = 1时,不放入集合
                if (count != 1) {
                    
                    // 遍历读取 构建 customer对象
                    String[] units = line.split(",");
                    Customer customer = new Customer();
                    customer.setCustomerCode(units[0]);
                    customer.setOrangeNum(Integer.parseInt(units[1]));
                    customer.setAllNum(customer.getOrangeNum());
                    
                    // 增加到集合中
                    customersList.add(customer);
                }
                count = 2;
            }
        } catch (NumberFormatException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        // 返回集合结果集
        return customersList;
    }
}

二、欧某信某外包公司

公司整体环境很好,属于中大型企业。面试官也比较年轻,应该是项目负责人之类的。

上来先是让做了下自我介绍。然后就开始针对简历上来问了。

1、你的专业不是计算机,你是自学的还是上过培训班?

其实这个问题,我不确定是想让我回答什么,我便回答是自学的。

2、调查系统用了几张表?项目中用到了哪些接口?

这个问题一时没有回答上来,写项目的时候,都是几个月前的事情了。这里面试的官的心理应该是需要确认一下是否项目是编的。这里我答不上来。

CRM项目 问卷调查系统
AUTHORITIES User
CONTACTS Survey
CUSTOMERS Question
ROLE_AUTHORITY Employee
SALES_CHANCES Bag
SALES_PLAN Answer
STORAGES Admin
PRODUCTS Auth
ORDER_ITEMS Log
CUSTOMER_ACTIVITIES Res
DICTS Role
CUSTOMER_DRAINS
ROLES
ORDERS
USERS
CUSTOMER_SERVICES

关于调用了哪些接口,这些接口是内部的还是外部的?

用的是Http协议吗?

虽然不知道如何回答:主要是用的RESTful API

3、Mysql数据库存储引擎的区别。

两种类型最主要的差别就是Innodb 支持事务处理与外键和行级锁。 而MyISAM不支持 。

MyISAM类型的表强调的是性能,其执行数度比InnoDB类型更快,但是不提供事务支持,而InnoDB提供事务支持以及外部键等高级数据库功能 。

对比项 MyISAM InnoDB 备注
主外键 不支持 支持
事务 不支持 支持
行表锁 表锁,即使操作一条记录也会锁住整个表,不适合高并发的操作 行锁,操作时只锁某一行,不对其它行有影响,适合高并发的操作
缓存 只缓存索引 有决定性的影响不仅缓存索引还要缓存真实数据,对内存要求较高,而且内存大小对性能
表空间
关注点 性能 事务
默认安装

其他注意点:

  • InnoDB不支持FULLTEXT类型的索引。

  • InnoDB 中不保存表的具体行数,也就是说,执行select count() from table时,InnoDB要扫描一遍整个表来计算有多少行,但是MyISAM只要简单的读出保存好的行数即可。注意的是,当count()语句包含 where条件时,两种表的操作是一样的。

  • 对于AUTO_INCREMENT类型的字段,InnoDB中必须包含只有该字段的索引,但是在MyISAM表中,可以和其他字段一起建立联合索引。

  • DELETE FROM table时,InnoDB不会重新建立表,而是一行一行的删除。

  • LOAD TABLE FROM MASTER操作对InnoDB是不起作用的,解决方法是首先把InnoDB表改成MyISAM表,导入数据后再改成InnoDB表,但是对于使用的额外的InnoDB特性(例如外键)的表不适用。

  • 另外,InnoDB表的行锁也不是绝对的,假如在执行一个SQL语句时MySQL不能确定要扫描的范围,InnoDB表同样会锁全表,例如update table set num=1 where name like “%aaa%”

3.1 如何选用?

MYISAM和INNODB是Mysql数据库提供的两种存储引擎。两者的优劣可谓是各有千秋。INNODB会支持一些关系数据库的高级功能,如事务功能和行级锁,MYISAM不支持。MYISAM的性能更优,占用的存储空间少。所以,选择何种存储引擎,视具体应用而定。

如果你的应用程序一定要使用事务,毫无疑问你要选择INNODB引擎。但要注意,INNODB的行级锁是有条件的。在where条件没有使用主键时,照样会锁全表。比如DELETE FROM mytable这样的删除语句。

如果你的应用程序对查询性能要求较高,就要使用MYISAM了。MYISAM索引和数据是分开的,而且其索引是压缩的,可以更好地利用内存。所以它的查询性能明显优于INNODB。压缩后的索引也能节约一些磁盘空间。MYISAM拥有全文索引的功能,这可以极大地优化LIKE查询的效率。

采用MyISAM引擎 采用InnoDB引擎
R/W > 100:1 且update相对较少<br />并发不高<br />表数据量小<br />硬件资源有限 R/W比较小,频繁更新大字段<br />表数据量超过1000万,并发高<br />安全性和可用性要求高

4、如何理解mysql的主从复制,分区分表?

4.1 分区表的原理

4.1.1 工作原理

对用户而言,分区表是一个独立的逻辑表,但是底层 MYSQL将其分成了多个物理子表,这对用户来说是透明的,每一个分区表都会使用一个独立的表文件。

创建表时使用 partition by子句定义每个分区存放的数据,执行查询时,优化器会根据分区定义过滤那些没有我们需要数据的分区,这样查询只需要查询所需数据在的分区。

分区的主要目的是将数据按照一个较粗的粒度分在不同的表中,这样可以将相关的数据存放在一起,而且如果想一次性删除整个分区的数据也很方便。

4.1.2 使用场景
  • 表非常大,无法全部存在内存,或者只在表的最后有热点数据其他都是历史数据
  • 分区表的数据更易维护,可以对独立的分区进行独立的操作
  • 分区表的数据可以分布在不同的机器上,从而高效使用资源
  • 可以使用分区表来避免某些特殊的瓶颈
  • 备份和恢复独立的分区
4.1.3 限制
  • 一个表最多只能有1024个分区
  • MySQL 5.1 版本中,分区表表达式必须是整数,5.5可以使用列分区
  • 分区字段中如果有主键和唯一索引列,那么主键列和唯一索引列都必须包含进来;
  • 分区表中无法使用外键约束
  • 需要对现有表的结构进行修改
  • 所有分区都必须使用相同的存储引擎
  • 分区函数中可以使用的函数和表达式会有一些限制
  • 某些存储引擎不支持分区
  • 对于MyISAM的分区表,不能使用load index into cache
  • 对于MyISAM表,使用分区表时需要打开更多的文件描述符

4.2 分库分表的原理

4.2.1 工作原理

通过一些hash算法或工具实现将一张数据表垂直或者水平进行物理分割。

4.2.2 使用场景
  • 单条记录条数达到百万到千万级别
  • 解决表锁的问题

4.3 分表方式

4.3.1 水平分割

表很大,分割后可以降低在查询时需要读的数据和索引的页数,同时也降低了索引的层数,提高查询速度

水平分割

使用场景

  • 表中的数据本身就有独立性,例如表中分别记录各个地区的数据或者不同时期的数据,特别是有些数据常用,有些不常用
  • 需要把数据存放在多个介质上

水平分表缺点

  • 给应用增加复杂度,通常查询时需要多个表名,查询所有数据都需 UNION操作
  • 在许多数据库应用中,这种复杂性会超过它带来的优点,查询时会增加读一个索引层的磁盘次数
4.3.2 垂直分割

把主键和一些列放在一个表,然后把主键和另外的列放在另一张表中。

垂直分割

使用场景

  • 如果一个表中某些列常用,而另外一些列不常用
  • 可以使数据行变小,一个数据页能存储更多数据,查询时减少I/O次数

垂直分表缺点

  • 管理冗余列,查询所有数据需要join操作

4.4 分表的缺点

  • 有些分表的策略基于应用层的逻辑算法,一旦逻辑算法改变,整个分表逻辑都会改变,扩展性较差
  • 对于应用层来说,逻辑算法无疑增加开发成本

4.5 延伸:MySQL的主从复制原理及负载均衡

4.5.1MySQL的主从复制原理
MySQL的主从复制原理
  • master 将该改变的记录到二进制日志(binary log)中。二进制日志事件 ,binary log events;
  • slave 将master 的 binary log events 拷贝到他的中继日志 (relay log);
  • slave 重做中继日志中的事件,将改变应用到自己的数据库中,mysql 复制是异步的且串行化的

4.5.2 原则

  • 每个slave 只用一个master
  • 每个slave 只能有一个唯一的服务器ID
  • 每个master 可以有多个salve
4.5.2 主从复制解决的问题
  • 数据分布:随意停止或开始复制,并在不同地理位置分布数据备份

  • 负载均衡:降低单个服务器的压力

  • 高可用和故障切换:帮助应用程序避免单点失败

  • 升级测试:可以使用更高版本的 MYSQL作为从库

    1]: https://blog.csdn.net/a724888/article/details/79393916 "Mysql主从复制,读写分离,分表分库策略与实践"

5、map接口常用的实现类有哪些,currenthashmap是怎么实现线程安全的。

Map接口的常用实现类 特点
HashMap k-v都允许为空
LinkedHashMap 额外维护一张表,保证存取顺寻
HashTable 线程安全的
TreeMap 可以按照Key的制定属性排序

5.1 线程安全的map

5.1.1 HashTable

可以看到大部分都用到了synchronized 关键字来修饰。说明他们是方法级别阻塞的,他们占用共享资源锁,效率比较低。

@SuppressWarnings("unchecked")
public synchronized V get(Object key) {
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            return (V)e.value;
        }
    }
    return null;
}

public synchronized V put(K key, V value) {
        // Make sure the value is not null
        if (value == null) {
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }

        addEntry(hash, key, value, index);
        return null;
    }
5.1.2 SynchronizedMap
他是一个Conllections的一个内部类,实现了Map<K,V>接口,将所有Map的方法用了synchronized修饰。
private static class SynchronizedMap<K,V>
    implements Map<K,V>, Serializable {
    private static final long serialVersionUID = 1978198479659022715L;

    private final Map<K,V> m;     // Backing Map
    final Object      mutex;        // Object on which to synchronize

    SynchronizedMap(Map<K,V> m) {
        this.m = Objects.requireNonNull(m);
        mutex = this;
    }

    SynchronizedMap(Map<K,V> m, Object mutex) {
        this.m = m;
        this.mutex = mutex;
    }

    public int size() {
        synchronized (mutex) {return m.size();}
    }
    public boolean isEmpty() {
        synchronized (mutex) {return m.isEmpty();}
    }
    public boolean containsKey(Object key) {
        synchronized (mutex) {return m.containsKey(key);}
    }
    public boolean containsValue(Object value) {
        synchronized (mutex) {return m.containsValue(value);}
    }
    public V get(Object key) {
        synchronized (mutex) {return m.get(key);}
    }

    public V put(K key, V value) {
        synchronized (mutex) {return m.put(key, value);}
    }
    public V remove(Object key) {
        synchronized (mutex) {return m.remove(key);}
    }
    public void putAll(Map<? extends K, ? extends V> map) {
        synchronized (mutex) {m.putAll(map);}
    }
    public void clear() {
        synchronized (mutex) {m.clear();}
    }

    private transient Set<K> keySet;
    private transient Set<Map.Entry<K,V>> entrySet;
    private transient Collection<V> values;

    public Set<K> keySet() {
        synchronized (mutex) {
            if (keySet==null)
                keySet = new SynchronizedSet<>(m.keySet(), mutex);
            return keySet;
        }
    }

    public Set<Map.Entry<K,V>> entrySet() {
        synchronized (mutex) {
            if (entrySet==null)
                entrySet = new SynchronizedSet<>(m.entrySet(), mutex);
            return entrySet;
        }
    }

    public Collection<V> values() {
        synchronized (mutex) {
            if (values==null)
                values = new SynchronizedCollection<>(m.values(), mutex);
            return values;
        }
    }

    public boolean equals(Object o) {
        if (this == o)
            return true;
        synchronized (mutex) {return m.equals(o);}
    }
    public int hashCode() {
        synchronized (mutex) {return m.hashCode();}
    }
    public String toString() {
        synchronized (mutex) {return m.toString();}
    }

    // Override default methods in Map
    @Override
    public V getOrDefault(Object k, V defaultValue) {
        synchronized (mutex) {return m.getOrDefault(k, defaultValue);}
    }
    @Override
    public void forEach(BiConsumer<? super K, ? super V> action) {
        synchronized (mutex) {m.forEach(action);}
    }
    @Override
    public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
        synchronized (mutex) {m.replaceAll(function);}
    }
    @Override
    public V putIfAbsent(K key, V value) {
        synchronized (mutex) {return m.putIfAbsent(key, value);}
    }
    @Override
    public boolean remove(Object key, Object value) {
        synchronized (mutex) {return m.remove(key, value);}
    }
    @Override
    public boolean replace(K key, V oldValue, V newValue) {
        synchronized (mutex) {return m.replace(key, oldValue, newValue);}
    }
    @Override
    public V replace(K key, V value) {
        synchronized (mutex) {return m.replace(key, value);}
    }
    @Override
    public V computeIfAbsent(K key,
            Function<? super K, ? extends V> mappingFunction) {
        synchronized (mutex) {return m.computeIfAbsent(key, mappingFunction);}
    }
    @Override
    public V computeIfPresent(K key,
            BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
        synchronized (mutex) {return m.computeIfPresent(key, remappingFunction);}
    }
    @Override
    public V compute(K key,
            BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
        synchronized (mutex) {return m.compute(key, remappingFunction);}
    }
    @Override
    public V merge(K key, V value,
            BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
        synchronized (mutex) {return m.merge(key, value, remappingFunction);}
    }

    private void writeObject(ObjectOutputStream s) throws IOException {
        synchronized (mutex) {s.defaultWriteObject();}
    }
}
5.1.3 ConcurrentHashMap

在1.7时是通过Segment+HashEntry的方式实现,ConcurrentHashMap初始化时,计算出Segment数组的大小ssize和每个Segment中HashEntry数组的大小cap,并初始化Segment数组的第一个元素。其中Segment在实现上继承了ReentrantLock,这样就自带了锁的功能。当执行put方法插入数据时,根据key的hash值,在Segment数组中找到相应的位置,如果相应位置的Segment还未初始化,则通过CAS进行赋值,接着执行Segment对象的put方法通过加锁机制插入数据。

1.8则采用==Node + CAS + Synchronized==来保证并发安全进行实现。

//1.7实现
数据结构
jdk1.7中采用Segment + HashEntry的方式进行实现
ConcurrentHashMap初始化时,计算出Segment数组的大小ssize和每个Segment中HashEntry数组的大小cap,并初始化Segment数组的第一个元素;其中ssize大小为2的幂次方,默认为16,cap大小也是2的幂次方,最小值为2,最终结果根据根据初始化容量initialCapacity进行计算,计算过程如下:
    if (c * ssize < initialCapacity)
        ++c;
    int cap = MIN_SEGMENT_TABLE_CAPACITY;
    while (cap < c)
        cap <<= 1;
其中Segment在实现上继承了ReentrantLock,这样就自带了锁的功能。

put实现
当执行put方法插入数据时,根据key的hash值,在Segment数组中找到相应的位置,如果相应位置的Segment还未初始化,则通过CAS进行赋值,接着执行Segment对象的put方法通过加锁机制插入数据,实现如下:

场景:线程A和线程B同时执行相同Segment对象的put方法

1、线程A执行tryLock()方法成功获取锁,则把HashEntry对象插入到相应的位置;
2、线程B获取锁失败,则执行scanAndLockForPut()方法,在scanAndLockForPut方法中,会通过重复执行tryLock()方法尝试获取锁,在多处理器环境下,重复次数为64,单处理器重复次数为1,当执行tryLock()方法的次数超过上限时,则执行lock()方法挂起线程B;
3、当线程A执行完插入操作时,会通过unlock()方法释放锁,接着唤醒线程B继续执行;

//1.8实现
采用Node + CAS + Synchronized来保证并发安全进行实现,结构如上:
只有在执行第一次put方法时才会调用initTable()初始化Node数组,
put实现
当执行put方法插入数据时,根据key的hash值,在Node数组中找到相应的位置,实现如下:

1、如果相应位置的Node还未初始化,则通过CAS插入相应的数据;

else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
    if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
        break;                   // no lock when adding to empty bin
}

2、如果相应位置的Node不为空,且当前该节点不处于移动状态,则对该节点加synchronized锁,如果该节点的hash不小于0,则遍历链表更新节点或插入新节点;
if (fh >= 0) {
    binCount = 1;
    for (Node<K,V> e = f;; ++binCount) {
        K ek;
        if (e.hash == hash &&
            ((ek = e.key) == key ||
             (ek != null && key.equals(ek)))) {
            oldVal = e.val;
            if (!onlyIfAbsent)
                e.val = value;
            break;
        }
        Node<K,V> pred = e;
        if ((e = e.next) == null) {
            pred.next = new Node<K,V>(hash, key, value, null);
            break;
        }
    }
}

3、如果该节点是TreeBin类型的节点,说明是红黑树结构,则通过putTreeVal方法往红黑树中插入节点;
else if (f instanceof TreeBin) {
    Node<K,V> p;
    binCount = 2;
    if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
        oldVal = p.val;
        if (!onlyIfAbsent)
            p.val = value;
    }
}

4、如果binCount不为0,说明put操作对数据产生了影响,如果当前链表的个数达到8个,则通过treeifyBin方法转化为红黑树,如果oldVal不为空,说明是一次更新操作,没有对元素个数产生影响,则直接返回旧值;

if (binCount != 0) {
    if (binCount >= TREEIFY_THRESHOLD)
        treeifyBin(tab, i);
    if (oldVal != null)
        return oldVal;
    break;
}

5、如果插入的是一个新节点,则执行addCount()方法尝试更新元素个数baseCount;

6、Redis的五种数据结构?

数据结构 说明
String 二进制安全的
Hash string类型的field和value的映射表
Set string类型的无序集合 不允许重复 通过哈希表实现<br /> 集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。
Zset string类型元素的集合, 每个元素都会关联一个double类型的分数<br /> redis正是通过分数来为集合中的成员进行从小到大的排序。 <br /> zset的成员是唯一的,但分数(score)却可以重复。
List 插入顺序排序

7、跨域怎么实现?

浏览器的同源策略

提到跨域不能不先说一下”同源策略”。
何为同源?只有当协议端口、和域名都相同的页面,则两个页面具有相同的源。只要网站的 协议名protocol、 主机host、 端口号port 这三个中的任意一个不同,网站间的数据请求与传输便构成了跨域调用,会受到同源策略的限制。
同源策略限制从一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的关键的安全机制。浏览器的同源策略,出于防范跨站脚本的攻击,禁止客户端脚本(如 JavaScript)对不同域的服务进行跨站调用(通常指使用XMLHttpRequest请求)。

跨域请求方式

解决跨域问题,最简单的莫过于通过nginx反向代理进行实现,但是其需要在运维层面修改,且有可能请求的资源并不再我们控制范围内(第三方),所以该方式不能作为通用的解决方案,下面阐述了经常用到几种跨域方式:

方式一:图片ping或script标签跨域

图片ping常用于跟踪用户点击页面或动态广告曝光次数。
script标签可以得到从其他来源数据,这也是JSONP依赖的根据。
缺点:只能发送Get请求 ,无法访问服务器的响应文本(单向请求)

方式二:JSONP跨域

JSONPJSON with Padding)是数据格式JSON的一种“使用模式”,可以让网页从别的网域要数据。根据 XmlHttpRequest 对象受到同源策略的影响,而利用 ``元素的这个开放策略,网页可以得到从其他来源动态产生的JSON数据,而这种使用模式就是所谓的 JSONP。用JSONP抓到的数据并不是JSON,而是任意的JavaScript,用 JavaScript解释器运行而不是用JSON解析器解析。所有,通过Chrome查看所有JSONP发送的Get请求都是js类型,而非XHR。

image

缺点:

  • 只能使用Get请求
  • 不能注册success、error等事件监听函数,不能很容易的确定JSONP请求是否失败
  • JSONP是从其他域中加载代码执行,容易受到跨站请求伪造的攻击,其安全性无法确保
方式三:CORS

Cross-Origin Resource Sharing(CORS)跨域资源共享是一份浏览器技术的规范,提供了 Web 服务从不同域传来沙盒脚本的方法,以避开浏览器的同源策略,确保安全的跨域数据传输。现代浏览器使用CORS在API容器如XMLHttpRequest来减少HTTP请求的风险来源。与 JSONP 不同,CORS 除了 GET 要求方法以外也支持其他的 HTTP 要求。服务器一般需要增加如下响应头的一种或几种:

Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 864001234

跨域请求默认不会携带Cookie信息,如果需要携带,请配置下述参数:

"Access-Control-Allow-Credentials": true
// Ajax设置
"withCredentials": true123
方式四:window.name+iframe

window.name通过在iframe(一般动态创建i)中加载跨域HTML文件来起作用。然后,HTML文件将传递给请求者的字符串内容赋值给window.name。然后,请求者可以检索window.name值作为响应。

  • iframe标签的跨域能力;
  • window.name属性值在文档刷新后依旧存在的能力(且最大允许2M左右)。

每个iframe都有包裹它的window,而这个window是top window的子窗口。contentWindow属性返回<iframe>元素的Window对象。你可以使用这个Window对象来访问iframe的文档及其内部DOM。

<!-- 
 下述用端口 
 10000表示:domainA
 10001表示:domainB
-->

<!-- localhost:10000 -->
<script>
  var iframe = document.createElement('iframe');
  iframe.style.display = 'none'; // 隐藏

  var state = 0; // 防止页面无限刷新
  iframe.onload = function() {
      if(state === 1) {
          console.log(JSON.parse(iframe.contentWindow.name));
          // 清除创建的iframe
          iframe.contentWindow.document.write('');
          iframe.contentWindow.close();
          document.body.removeChild(iframe);
      } else if(state === 0) {
          state = 1;
          // 加载完成,指向当前域,防止错误(proxy.html为空白页面)
          // Blocked a frame with origin "http://localhost:10000" from accessing a cross-origin frame.
          iframe.contentWindow.location = 'http://localhost:10000/proxy.html';
      }
  };

  iframe.src = 'http://localhost:10001';
  document.body.appendChild(iframe);
</script>

<!-- localhost:10001 -->
<!DOCTYPE html>
...
<script>
  window.name = JSON.stringify({a: 1, b: 2});
</script>
</html>1234567891011121314151617181920212223242526272829303132333435363738

注意:

  1. 直接嵌入其他域(localhots:10001)下的URL会报错,所以需要加载完成替换为当前域的URL(localhots:10000),proxy.html为空白页面,只为解决该问题;
    跨域请求iframe
  2. 重新设置src(http://localhost:10000/proxy.html)后导致页面不断刷新,所以通过state来控制;
  3. 全部获取完结果后,清除该iframe。
方式五:window.postMessage()

HTML5新特性,可以用来向其他所有的 window 对象发送消息。需要注意的是我们必须要保证所有的脚本执行完才发送 MessageEvent,如果在函数执行的过程中调用了它,就会让后面的函数超时无法执行。

下述代码实现了跨域存储localStorage

<!-- 
 下述用端口 
 10000表示:domainA
 10001表示:domainB
-->

<!-- localhost:10000 -->
<iframe src="http://localhost:10001/msg.html" name="myPostMessage" style="display:none;">
</iframe>

<script>
  function main() {
      LSsetItem('test', 'Test: ' + new Date());
      LSgetItem('test', function(value) {
          console.log('value: ' + value);
      });
      LSremoveItem('test');
  }

  var callbacks = {};
  window.addEventListener('message', function(event) {
      if (event.source === frames['myPostMessage']) {
          console.log(event)
          var data = /^#localStorage#(\d+)(null)?#([\S\s]*)/.exec(event.data);
          if (data) {
              if (callbacks[data[1]]) {
                  callbacks[data[1]](data[2] === 'null' ? null : data[3]);
              }
              delete callbacks[data[1]];
          }
      }
  }, false);

  var domain = '*';
  // 增加
  function LSsetItem(key, value) {
      var obj = {
          setItem: key,
          value: value
      };
      frames['myPostMessage'].postMessage(JSON.stringify(obj), domain);
  }
  // 获取
  function LSgetItem(key, callback) {
      var identifier = new Date().getTime();
      var obj = {
          identifier: identifier,
          getItem: key
      };
      callbacks[identifier] = callback;
      frames['myPostMessage'].postMessage(JSON.stringify(obj), domain);
  }
  // 删除
  function LSremoveItem(key) {
      var obj = {
          removeItem: key
      };
      frames['myPostMessage'].postMessage(JSON.stringify(obj), domain);
  }
</script>

<!-- localhost:10001 -->
<script>
  window.addEventListener('message', function(event) {
    console.log('Receiver debugging', event);
    if (event.origin == 'http://localhost:10000') {
      var data = JSON.parse(event.data);
      if ('setItem' in data) {
        localStorage.setItem(data.setItem, data.value);
      } else if ('getItem' in data) {
        var gotItem = localStorage.getItem(data.getItem);
        event.source.postMessage(
          '#localStorage#' + data.identifier +
          (gotItem === null ? 'null#' : '#' + gotItem),
          event.origin
        );
      } else if ('removeItem' in data) {
        localStorage.removeItem(data.removeItem);
      }
    }
  }, false);
</script>12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182

注意Safari下会报错:

Blocked a frame with origin “http://localhost:10001” from accessing a frame with origin “http://localhost:10000“. Protocols, domains, and ports must match.

避免该错误,可以在Safari浏览器中勾选开发菜单==>停用跨域限制。或者只能使用服务器端转存的方式实现,因为Safari浏览器默认只支持CORS跨域请求。

方式六:修改document.domain跨子域

前提条件:这两个域名必须属于同一个基础域名!而且所用的协议,端口都要一致,否则无法利用document.domain进行跨域,所以只能跨子域

根域范围内,允许把domain属性的值设置为它的上一级域。例如,在”aaa.xxx.com”域内,可以把domain设置为 “xxx.com” 但不能设置为 “xxx.org” 或者”com”。

现在存在两个域名aaa.xxx.com和bbb.xxx.com。在aaa下嵌入bbb的页面,由于其document.name不一致,无法在aaa下操作bbb的js。可以在aaa和bbb下通过js将document.name = 'xxx.com';设置一致,来达到互相访问的作用。

方式七:WebSocket

WebSocket protocol 是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很棒的实现。相关文章,请查看:WebSocketWebSocket-SockJS

需要注意:WebSocket对象不支持DOM 2级事件侦听器,必须使用DOM 0级语法分别定义各个事件。

方式八:代理

同源策略是针对浏览器端进行的限制,可以通过服务器端来解决该问题

DomainA客户端(浏览器) ==> DomainA服务器 ==> DomainB服务器 ==> DomainA客户端(浏览器)

实现HTTP、HTTPS代理请参照: 创建HTTP与HTTPS服务器与客户端

8、什么时候用序列化,深度复制和浅复制区别。

序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化,将数据分解成字节流,以便存储在文件中或在网络上传输。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。序列化是为了解决在对对象流进行读写操作时所引发的问题。 序列化的实现:将需要被序列化的类实现Serializable接口,该接口没有需要实现的方法,implements Serializable只是为了标注该对象是可被序列化的,然后使用一个输出流(如:FileOutputStream)来构造一个ObjectOutputStream(对象流)对象,接着,使用ObjectOutputStream对象的writeObject(Object obj)方法就可以将参数为obj的对象写出(即保存其状态),要恢复的话则用输入流;

序列化分为两大部分:

序列化和反序列化。序列化是这个过程的第一部分,将数据分解成字节流,以便存储在文件中或在网络上传输。反序列化就是打开字节流并重构对象。对象序列化不仅要将基本数据类型转换成字节表示,有时还要恢复数据。恢复数据要求有恢复数据的对象实例

序列化的特点:

如果某个类能够被序列化,其子类也可以被序列化。声明为static和transient类型的成员数据不能被序列化。因为static代表类的状态, transient代表对象的临时数据。

什么时候使用序列化:

  • 对象序列化可以实现分布式对象。主要应用例如:RMI要利用对象序列化运行远程主机上的服务,就像在本地机上运行对象时一样。 在网络中传输时。*
  • java对象序列化不仅保留一个对象的数据,而且递归保存对象引用的每个对象的数据。可以将整个对象层次写入字节流中,可以保存在文件中或在网络连接上传递。利用对象序列化可以进行对象的"深复制",即复制对象本身及引用的对象本身**。序列化一个对象可能得到整个对象序列。

序列化中的继承问题

  • 当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口。
  • 一个子类实现了 Serializable 接口,它的父类都没有实现 Serializable 接口,要想将父类对象也序列化,就需要让父类也实现Serializable 接口。

序列化相关的类:

java.io.Serializable
java.io.Externalizable
ObjectOutput
ObjectInput
ObjectOutputStream
ObjectInputStream

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

推荐阅读更多精彩内容