Developer

Developer 查看完整档案

武汉编辑东北财经大学  |  信息管理与信息系统 编辑航班管家  |  Java开发工程师 编辑 segmentfault.com/u/song_song 编辑
编辑

只要还在学习,人生就有无限的希望...

个人动态

Developer 发布了文章 · 7月30日

Executors线程池工具类

介绍

Executors: 对 ThreadPoolExecutorScheduledThreadPoolExecutor 封装的工具类,方便创建线程池。

但是《阿里巴巴Java开发手册》中有要求:

【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

所以不建议使用 Executors 类,直接使用 ThreadPoolExcutor 类有助于我们更明确底部规则,规避风险。

常用的几个方法:

Executors.newFixedThreadPool(int nThreads);

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

创建一个有固定线程数的线程池,如果任务数大于了最大线程数,则其它任务将在队列中排列等待。

不过该队列new LinkedBlockingQueue<Runnable>()的长度为 Integer.MAX_VALUE,极端情况下,可能会推积大量请求,从而导致OOM。

Executors.newSingleThreadExecutor();

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

只会创建一条工作线程处理任务,不过该队列new LinkedBlockingQueue<Runnable>()的长度为 Integer.MAX_VALUE,极端情况下,可能会推积大量请求,从而导致OOM。

Executors.newFixedThreadPool(int nThreads) 不完全一样,可参考这篇文章《关于SingleThreadExecutor以及FinalizableDelegatedExecutorService》

Executors.newCachedThreadPool();

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

创建一个有60s缓存的线程池。该线程可以根据需要智能的创建新的线程,或者重用空闲但未过缓存期的线程。
如果线程池中有超过缓存期的线程(60s不执行任务),该闲置的线程将会被终止并从线程池中移除。

不过,该线程池的最大线程数量是 Integer.MAX_VALUE,极端情况下,可能会推积大量请求,从而导致OOM。

Executors.newScheduledThreadPool(int corePoolSize);

创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。

不过,该线程池的最大线程数量是 Integer.MAX_VALUE,极端情况下,可能会推积大量请求,从而导致OOM。

查看原文

赞 0 收藏 0 评论 0

Developer 发布了文章 · 2019-07-08

MySQL存储引擎概览

查看mysql版本

mysql> select version();
+------------+
| version()  |
+------------+
| 5.6.32-log |
+------------+
1 row in set (0.00 sec)

或者:

mysql> status;
--------------
mysql  Ver 14.14 Distrib 5.6.32, for Win32 (AMD64)

Connection id:          9
Current database:
Current user:           root@localhost
SSL:                    Not in use
Using delimiter:        ;
Server version:         5.6.32-log MySQL Community Server (GPL)
Protocol version:       10
Connection:             localhost via TCP/IP
Server characterset:    latin1
Db     characterset:    latin1
Client characterset:    gbk
Conn.  characterset:    gbk
TCP port:               3307
Uptime:                 4 hours 7 min 11 sec

Threads: 4  Questions: 126  Slow queries: 0  Opens: 73  Flush tables: 1  Open tables: 66  Queries per second avg: 0.008

查看MySQL存储引擎

show engines
EngineSupportCommentTransactionsXASavepoints
MyISAMYESMyISAM storage engineNONONO
MRG_MYISAMYESCollection of identical MyISAM tablesNONONO
CSVYESCSV storage engineNONONO
BLACKHOLEYES/dev/null storage engine (anything you write to it disappears)NONONO
MEMORYYESHash based, stored in memory, useful for temporary tablesNONONO
FEDERATEDNOFederated MySQL storage engine(NULL)(NULL)(NULL)
ARCHIVEYESArchive storage engineNONONO
InnoDBDEFAULTSupports transactions, row-level locking, and foreign keysYESYESYES
PERFORMANCE_SCHEMAYESPerformance SchemaNONONO

Support:DEFAULT表示MYSQL数据库默认的存储引擎,其它存储引擎就是你可以选择的。
如上表,InnoDB是默认的存储引擎

存储引擎中InnoDB与Myisam的主要区别

1. 事务处理

innodb支持事务功能,myisam 不支持。
Myisam的执行速度更快,性能更好。

2. select ,update,insert ,delete 操作

MyISAM:如果执行大量的SELECT,MyISAM是更好的选择
InnoDB:如果你的数据执行大量的INSERT或UPDATE,出于性能方面的考虑,应该使用InnoDB表

3. 锁机制不同

InnoDB 为行级锁,myisam为表级锁。
注意:当数据库无法确定,所找的行时,也会变为锁定整个表。
如: update table set num = 10 where username like "%test%";

4. 查询表的行数不同

select count(*) fromtable

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

5. 物理结构不同

MyISAM :每个MyISAM在磁盘上存储成三个文件。第一个文件的名字以表的名字开始,扩展名指出文件类型。

  • .frm文件存储表定义。
  • 数据文件的扩展名为.MYD (MYData)。
  • 索引文件的扩展名是.MYI(MYIndex)

InnoDB:基于磁盘的资源是InnoDB表空间数据文件和它的日志文件,InnoDB 表的大小只受限于操作系统文件的大小,一般为2GB

6. anto_increment机制不同

更好和更快的auto_increment处理

其他:为什么MyISAM会比Innodb的查询速度快。

Innodb在做SELECT的时候,要维护的东西比MYISAM引擎多很多;

  • 数据块,Innodb要缓存,MYISAM只缓存索引块, 这中间还有换进换出的减少;
  • Innodb寻址要映射到块,再到行,MYISAM记录的直接是文件的OFFSET,定位比Innodb要快
  • Innodb还需要维护MVCC一致;虽然你的场景没有,但他还是需要去检查和维护

MVCC (Multi-Version Concurrency Control)多版本并发控制

InnoDB:通过为每一行记录添加两个额外的隐藏的值来实现MVCC,这两个值一个记录这行数据何时被创建,另外一个记录这行数据何时过期(或者被删除)。但是InnoDB并不存储这些事件发生时的实际时间,相反它只存储这些事件发生时的系统版本号。这是一个随着事务的创建而不断增长的数字。每个事务在事务开始时会记录它自己的系统版本号。每个查询必须去检查每行数据的版本号与事务的版本号是否相同。让我们来看看当隔离级别是REPEATABLEREAD时这种策略是如何应用到特定的操作的:

SELECT InnoDB必须每行数据来保证它符合两个条件:
  1、InnoDB必须找到一个行的版本,它至少要和事务的版本一样老(也即它的版本号不大于事务的版本号)。这保证了不管是事务开始之前,或者事务创建时,或者修改了这行数据的时候,这行数据是存在的。
  2、这行数据的删除版本必须是未定义的或者比事务版本要大。这可以保证在事务开始之前这行数据没有被删除。

查看原文

赞 1 收藏 0 评论 0

Developer 发布了文章 · 2019-07-02

JVM 栈(stack)溢出案例

介绍

当启动一个新线程时,JVM就会给这个线程分配一个Java栈(这个栈的内存大小由-Xss参数来设置)。

一个Java栈的基本单位是帧,每一次函数调用就会生成栈帧,占用一定的栈空间。当函数本身需要的内存过大,或者函数调用函数(依赖调用或者递归调用)太深,超过了-Xss设置的内存大小,就会抛出StackOverflowError异常。

-Xss:默认值 1M,控制每个线程占用的内存,这个参数决定了函数调用的最大深度。如果设置的太小可能会很容易出现 StackOverflowError 异常。

JDK 5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。

示例代码

public class StackOverflow {

    private static int deep = 1;

    /**
     * 通过无限递归来模拟栈溢出
     */
    private static void recursion() {
        deep++;
        recursion();
    }

    public static void main(String[] args) {
        try {
            recursion();
        } catch (Throwable e) { // catch 捕获的是 Throwable,而不是 Exception。因为 StackOverflowError 不属于 Exception 的子类。
            System.out.println("Stack deep : " + deep);
            e.printStackTrace();
        }

        // 不让进程结束,便于使用分析工具来查看内存情况
        try {
            Thread.sleep(24 * 60 * 60 * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

执行结果

Stack deep : 11429
java.lang.StackOverflowError
    at com.song.StackOverflow.call(StackOverflow.java:16)
    at com.song.StackOverflow.call(StackOverflow.java:17)
    ...
查看原文

赞 1 收藏 1 评论 0

Developer 发布了文章 · 2019-07-01

JVM 堆(heap)溢出案例

一、说明

当虚拟机申请不到内存空间的时候,会报堆内存溢出: OutOfMemoryError:java heap space

常见的原因:http://outofmemory.cn/c/java-...

我测试到时候,运行在 16G 内存的机器上。JVM 堆内存 默认为物理内存的1/4,即 16 * 1/4 = 4G

JDK 8的 JVM 在 JDK 7 的基础上从堆内存中移除了永久代(Perm Generation),替换为了堆内存之外的元空间(Metaspace),元空间是堆外直接内存,不受堆内存的限制,只受物理内存的限制,可以提供更大的空间。

二、原因及解决办法

OutOfMemoryError 异常的常见原因:

  1. 加载的数据过大。如:加载的文件或者图片过大、一次从数据库取出过多数据
  2. 代码存在死循环或循环产生过多的对象

解决方法

  1. 增加jvm的内存大小,使用 -Xmx 和 -Xms 来设置
  2. 检查代码中是否有死循环或递归调用。
  3. 检查是否有大循环重复产生新对象实体。
  4. 检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
  5. 检查List、Map等集合对象是否有使用完后,未清除的问题。List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。

三、代码示例

/**java堆溢出实例
 * 原理:java的堆是用来存放对象实例的,所以我们只要做到以下三点就可以使堆溢出:
 * 1、限制堆的大小,不可扩展
 * 2、不断新建对象
 * 3、保持对象存活不被回收
 * 对应的,我们需要:
 * 1、改变JVM的启动参数,将堆的最小值和最大值设成一样,这样就可以避免堆自动扩展(其实不一样也可以)
 * 2、不断产生对象
 * 3、使用一个List来保存对象,保持对象存活
 *
 * JVM配置参数: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
 *
 */
public class HeapOom {
    public static void main(String[] args) {
        // 此list实例会存放在堆内存中
        List<byte[]> list = new ArrayList<>();
        int i = 0;
        boolean flag = true;
        while (flag) {
            try {
                i++;
                // 每次增加一个1M大小的数组对象
                list.add(new byte[1024 * 1024]);
            } catch (Throwable e) {  // catch 捕获的是 Throwable,而不是 Exception。因为 OutOfMemoryError 不属于 Exception 的子类。
                e.printStackTrace();
                flag = false;
                // 记录次数
                System.out.println("count=" + i);
            }
        }

        // 不让进程结束,便于使用分析工具来查看内存情况
        try {
            Thread.sleep(24 * 60 * 60 * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

使用的 java 1.8.0_171 版本:

$ java -version
java version "1.8.0_171"
Java(TM) SE Runtime Environment (build 1.8.0_171-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.171-b11, mixed mode)

报错信息:

count=3316
java.lang.OutOfMemoryError: Java heap space
    at com.song.HeapOom.main(HeapOom.java:21)

运行结果表明,运行到 3316 次时,出现了堆内存溢出。由于每次增加1M内存。粗略估计:程序大约使用3G内存时,出现了内存溢出情况。

通过jmap或VisualVM 者工具可以查看内存情况:最后可以看出,老年代内存使用情况,大约使用了2709MB,使用率近100%。从而导致了 OutOfMemoryError

TIPS:下面查看内存时,由于使用工具查看内存的时间不是同一时间,所以内存使用量有细微差别

1. 使用jmap查看的内存信息

$ jmap -heap 3428
Attaching to process ID 3428, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.66-b18

using thread-local object allocation.
Parallel GC with 4 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 4261412864 (4064.0MB)
   NewSize                  = 88604672 (84.5MB)
   MaxNewSize               = 1420296192 (1354.5MB)
   OldSize                  = 177733632 (169.5MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 637009920 (607.5MB)
   used     = 637009920 (607.5MB)
   free     = 0 (0.0MB)
   100.0% used
From Space:
   capacity = 11010048 (10.5MB)
   used     = 0 (0.0MB)
   free     = 11010048 (10.5MB)
   0.0% used
To Space:
   capacity = 11010048 (10.5MB)
   used     = 0 (0.0MB)
   free     = 11010048 (10.5MB)
   0.0% used
PS Old Generation  // 老年代内存使用情况,大约使用了2709MB,使用率近100%(导致OutOfMemoryError)
   capacity = 2841116672 (2709.5MB)
   used     = 2840991320 (2709.38045501709MB)
   free     = 125352 (0.11954498291015625MB)
   99.99558793198338% used

4955 interned Strings occupying 422328 bytes.

2. 使用 VisualVM 查看的内存信息

clipboard.png

clipboard.png

clipboard.png

查看原文

赞 0 收藏 0 评论 0

Developer 评论了文章 · 2019-04-30

SpringBoot 2.0集成spring-data-elasticsearch

一、配置

spring-boot 2.0.2
spring-data-elasticsearch 3.0.7
elasticsearch 5.6.9

1. Maven依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.2.RELEASE</version>
        <relativePath/>
    </parent>
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>
    </dependencies>
</project>

2. application.yml配置

spring:
   data:
        elasticsearch:
            #cluster-name: #默认为elasticsearch
            cluster-nodes: 127.0.0.1:9300 #配置es节点信息,逗号分隔,如果没有指定,则启动ClientNode(9200端口是http查询使用的。9300集群使用。这里使用9300.)
            properties:
                path:
                  logs: ./elasticsearch/log #elasticsearch日志存储目录
                  data: ./elasticsearch/data #elasticsearch数据存储目录

二、使用

官方文档:https://docs.spring.io/spring...
中文翻译:https://www.jianshu.com/p/27e...
入门参考:https://www.cnblogs.com/guozp...

1. @Document

@Document注解里面的几个属性,类比mysql的话是这样:

index –> DB   
type –> Table   
Document –> row   

加上@Id注解后,在Elasticsearch里对应的该列就是主键了,在查询时就可以直接用主键查询。其实和mysql非常类似,基本就是一个数据库。

@Persistent
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface Document {
    String indexName();//索引库的名称,个人建议以项目的名称命名
    String type() default "";//类型,个人建议以实体的名称命名
    short shards() default 5;//默认分区数
    short replicas() default 1;//每个分区默认的备份数
    String refreshInterval() default "1s";//刷新间隔
    String indexStoreType() default "fs";//索引文件存储类型
}

2. @Field

加上了@Document注解之后,默认情况下这个实体中所有的属性都会被建立索引、并且分词。
通过@Field注解来进行详细的指定,如果没有特殊需求,那么只需要添加@Document即可。

@Field注解的定义如下:  

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
@Inherited
public @interface Field {

    FieldType type() default FieldType.Auto;#自动检测属性的类型
    FieldIndex index() default FieldIndex.analyzed;#默认情况下分词
    DateFormat format() default DateFormat.none;
    String pattern() default "";
    boolean store() default false;#默认情况下不存储原文
    String searchAnalyzer() default "";#指定字段搜索时使用的分词器
    String indexAnalyzer() default "";#指定字段建立索引时指定的分词器
    String[] ignoreFields() default {};#如果某个字段需要被忽略
    boolean includeInParent() default false;
}

3. ElasticsearchRepository

//不需要加@Component,直接可以@Autowared
public interface ArticleSearchRepository extends ElasticsearchRepository<Article, Long> {
    List<Country> findByName(String name);
    //使用 Page<Country> countrys = articleSearchRepository.findByName("测试",  PageRequest.of(0, 10)); //分页是从0开始的
    Page<Country> findByName(String name, Pageable pageable); 
    Country findProductById(String name);

}

Page的方法:

  • getTotalElements() 匹配的总共有多少条数据
  • getTotalPages() 匹配的总共有多少页
  • getSize() 用户想在当前页获取的数量
  • getNumberOfElements() 当前页实际获取的数量
  • getPageable().getPageSize() 当前页获取的数量
  • getPageable().getPageNumber() 当前是多少页(从0开始,使用的时候需要+1)

4. 示例

Country.java

@Document(indexName = "world", type = "country")
public class Country implements Serializable {

    @Id
    private Integer id;

    @Field(searchAnalyzer = "ik_max_word",analyzer = "ik_smart")
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }
}

CountrySearchRepository.java

public interface CountrySearchRepository extends ElasticsearchRepository<Country, Long> {
    List<Country> findCountryByName(String name);
    //使用 Page<Country> countrys = countrySearchRepository.findByName("测试",  PageRequest.of(0, 10)); //分页是从0开始的
    Page<Country> findCountryByName(String name, Pageable pageable); 
    Country findCountryById(String name);

}

SearchService.java

public class SearchService{

    @Autowared
    CountrySearchRepository countrySearchRepository;
  
    public Page<Country> getCountryByName(String name) {
        Page<Country> countrys = countrySearchRepository.findCountryByName("测试",  PageRequest.of(0, 10));
        return countrys;
    }
}
查看原文

Developer 评论了文章 · 2019-04-30

SpringBoot 2.0集成spring-data-elasticsearch

一、配置

spring-boot 2.0.2
spring-data-elasticsearch 3.0.7
elasticsearch 5.6.9

1. Maven依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.2.RELEASE</version>
        <relativePath/>
    </parent>
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>
    </dependencies>
</project>

2. application.yml配置

spring:
   data:
        elasticsearch:
            #cluster-name: #默认为elasticsearch
            cluster-nodes: 127.0.0.1:9300 #配置es节点信息,逗号分隔,如果没有指定,则启动ClientNode(9200端口是http查询使用的。9300集群使用。这里使用9300.)
            properties:
                path:
                  logs: ./elasticsearch/log #elasticsearch日志存储目录
                  data: ./elasticsearch/data #elasticsearch数据存储目录

二、使用

官方文档:https://docs.spring.io/spring...
中文翻译:https://www.jianshu.com/p/27e...
入门参考:https://www.cnblogs.com/guozp...

1. @Document

@Document注解里面的几个属性,类比mysql的话是这样:

index –> DB   
type –> Table   
Document –> row   

加上@Id注解后,在Elasticsearch里对应的该列就是主键了,在查询时就可以直接用主键查询。其实和mysql非常类似,基本就是一个数据库。

@Persistent
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface Document {
    String indexName();//索引库的名称,个人建议以项目的名称命名
    String type() default "";//类型,个人建议以实体的名称命名
    short shards() default 5;//默认分区数
    short replicas() default 1;//每个分区默认的备份数
    String refreshInterval() default "1s";//刷新间隔
    String indexStoreType() default "fs";//索引文件存储类型
}

2. @Field

加上了@Document注解之后,默认情况下这个实体中所有的属性都会被建立索引、并且分词。
通过@Field注解来进行详细的指定,如果没有特殊需求,那么只需要添加@Document即可。

@Field注解的定义如下:  

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
@Inherited
public @interface Field {

    FieldType type() default FieldType.Auto;#自动检测属性的类型
    FieldIndex index() default FieldIndex.analyzed;#默认情况下分词
    DateFormat format() default DateFormat.none;
    String pattern() default "";
    boolean store() default false;#默认情况下不存储原文
    String searchAnalyzer() default "";#指定字段搜索时使用的分词器
    String indexAnalyzer() default "";#指定字段建立索引时指定的分词器
    String[] ignoreFields() default {};#如果某个字段需要被忽略
    boolean includeInParent() default false;
}

3. ElasticsearchRepository

//不需要加@Component,直接可以@Autowared
public interface ArticleSearchRepository extends ElasticsearchRepository<Article, Long> {
    List<Country> findByName(String name);
    //使用 Page<Country> countrys = articleSearchRepository.findByName("测试",  PageRequest.of(0, 10)); //分页是从0开始的
    Page<Country> findByName(String name, Pageable pageable); 
    Country findProductById(String name);

}

Page的方法:

  • getTotalElements() 匹配的总共有多少条数据
  • getTotalPages() 匹配的总共有多少页
  • getSize() 用户想在当前页获取的数量
  • getNumberOfElements() 当前页实际获取的数量
  • getPageable().getPageSize() 当前页获取的数量
  • getPageable().getPageNumber() 当前是多少页(从0开始,使用的时候需要+1)

4. 示例

Country.java

@Document(indexName = "world", type = "country")
public class Country implements Serializable {

    @Id
    private Integer id;

    @Field(searchAnalyzer = "ik_max_word",analyzer = "ik_smart")
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }
}

CountrySearchRepository.java

public interface CountrySearchRepository extends ElasticsearchRepository<Country, Long> {
    List<Country> findCountryByName(String name);
    //使用 Page<Country> countrys = countrySearchRepository.findByName("测试",  PageRequest.of(0, 10)); //分页是从0开始的
    Page<Country> findCountryByName(String name, Pageable pageable); 
    Country findCountryById(String name);

}

SearchService.java

public class SearchService{

    @Autowared
    CountrySearchRepository countrySearchRepository;
  
    public Page<Country> getCountryByName(String name) {
        Page<Country> countrys = countrySearchRepository.findCountryByName("测试",  PageRequest.of(0, 10));
        return countrys;
    }
}
查看原文

Developer 评论了文章 · 2019-04-30

SpringBoot 2.0集成spring-data-elasticsearch

一、配置

spring-boot 2.0.2
spring-data-elasticsearch 3.0.7
elasticsearch 5.6.9

1. Maven依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.2.RELEASE</version>
        <relativePath/>
    </parent>
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>
    </dependencies>
</project>

2. application.yml配置

spring:
   data:
        elasticsearch:
            #cluster-name: #默认为elasticsearch
            cluster-nodes: 127.0.0.1:9300 #配置es节点信息,逗号分隔,如果没有指定,则启动ClientNode(9200端口是http查询使用的。9300集群使用。这里使用9300.)
            properties:
                path:
                  logs: ./elasticsearch/log #elasticsearch日志存储目录
                  data: ./elasticsearch/data #elasticsearch数据存储目录

二、使用

官方文档:https://docs.spring.io/spring...
中文翻译:https://www.jianshu.com/p/27e...
入门参考:https://www.cnblogs.com/guozp...

1. @Document

@Document注解里面的几个属性,类比mysql的话是这样:

index –> DB   
type –> Table   
Document –> row   

加上@Id注解后,在Elasticsearch里对应的该列就是主键了,在查询时就可以直接用主键查询。其实和mysql非常类似,基本就是一个数据库。

@Persistent
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface Document {
    String indexName();//索引库的名称,个人建议以项目的名称命名
    String type() default "";//类型,个人建议以实体的名称命名
    short shards() default 5;//默认分区数
    short replicas() default 1;//每个分区默认的备份数
    String refreshInterval() default "1s";//刷新间隔
    String indexStoreType() default "fs";//索引文件存储类型
}

2. @Field

加上了@Document注解之后,默认情况下这个实体中所有的属性都会被建立索引、并且分词。
通过@Field注解来进行详细的指定,如果没有特殊需求,那么只需要添加@Document即可。

@Field注解的定义如下:  

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
@Inherited
public @interface Field {

    FieldType type() default FieldType.Auto;#自动检测属性的类型
    FieldIndex index() default FieldIndex.analyzed;#默认情况下分词
    DateFormat format() default DateFormat.none;
    String pattern() default "";
    boolean store() default false;#默认情况下不存储原文
    String searchAnalyzer() default "";#指定字段搜索时使用的分词器
    String indexAnalyzer() default "";#指定字段建立索引时指定的分词器
    String[] ignoreFields() default {};#如果某个字段需要被忽略
    boolean includeInParent() default false;
}

3. ElasticsearchRepository

//不需要加@Component,直接可以@Autowared
public interface ArticleSearchRepository extends ElasticsearchRepository<Article, Long> {
    List<Country> findByName(String name);
    //使用 Page<Country> countrys = articleSearchRepository.findByName("测试",  PageRequest.of(0, 10)); //分页是从0开始的
    Page<Country> findByName(String name, Pageable pageable); 
    Country findProductById(String name);

}

Page的方法:

  • getTotalElements() 匹配的总共有多少条数据
  • getTotalPages() 匹配的总共有多少页
  • getSize() 用户想在当前页获取的数量
  • getNumberOfElements() 当前页实际获取的数量
  • getPageable().getPageSize() 当前页获取的数量
  • getPageable().getPageNumber() 当前是多少页(从0开始,使用的时候需要+1)

4. 示例

Country.java

@Document(indexName = "world", type = "country")
public class Country implements Serializable {

    @Id
    private Integer id;

    @Field(searchAnalyzer = "ik_max_word",analyzer = "ik_smart")
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }
}

CountrySearchRepository.java

public interface CountrySearchRepository extends ElasticsearchRepository<Country, Long> {
    List<Country> findCountryByName(String name);
    //使用 Page<Country> countrys = countrySearchRepository.findByName("测试",  PageRequest.of(0, 10)); //分页是从0开始的
    Page<Country> findCountryByName(String name, Pageable pageable); 
    Country findCountryById(String name);

}

SearchService.java

public class SearchService{

    @Autowared
    CountrySearchRepository countrySearchRepository;
  
    public Page<Country> getCountryByName(String name) {
        Page<Country> countrys = countrySearchRepository.findCountryByName("测试",  PageRequest.of(0, 10));
        return countrys;
    }
}
查看原文

Developer 发布了文章 · 2019-04-28

Docker搭建GitLab

官方中文教程:https://www.gitlab.com.cn/ins...
官方安装链接:https://about.gitlab.com/install
普通方式安装请见:https://segmentfault.com/a/11...

环境要求:内存至少4G,GitLab是很耗内存滴

一、安装

一般会将 GitLab 的配置 (etc) 、 日志 (log) 、数据 (data) 放到容器之外, 便于日后升级

docker pull gitlab/gitlab-ce:11.6.4-ce.0

# 通过docker run中加入环境变量,取名为gitlab
docker run --detach \       # 后台运行 -d
    # --hostname song.local \ # 指定容器域名,未知功能:创建镜像仓库的时候使用到
    -p 8443:443 \           # 将容器内443端口映射到主机8443,提供https服务
    -p 80:80 \              # 将容器内80端口映射到主机8080,提供http服务
    -p 10022:22 \           # 将容器内22端口映射到主机1002,提供ssh服务
    --name gitlab \         # 指定容器名称
    --restart=unless-stopped \                   # 容器运行中退出时(不是手动退出),自动重启
    --volume /var/lib/docker/volumes/gitlab-data/etc:/etc/gitlab \       # 将本地/var/lib/docker/volumes/gitlab-data/etc挂载到容器内/etc/gitlab
    --volume /var/lib/docker/volumes/gitlab-data/log:/var/log/gitlab \   # 将本地将本地/var/lib/docker/volumes/gitlab-data/log挂载到容器内/var/log/gitlab
    --volume /var/lib/docker/volumes/gitlab-data/data:/var/opt/gitlab \  # 将本地将本地/var/lib/docker/volumes/gitlab-data/data挂载到容器内/var/opt/gitlab
    gitlab/gitlab-ce:11.6.4-ce.0                 # 镜像名称:版本
为了方便日后启动,创建一个启动脚本:gitlab-docker-restart.sh,代码如下:
#!/bin/bash

serverName="gitlab"
imageName="gitlab/gitlab-ce:11.6.4-ce.0"

function runServer(){
    docker run --detach \
        -p 8443:443 \
        -p 80:80 \
        -p 10022:22 \
        --name ${serverName} \
        --volume /var/lib/docker/volumes/gitlab-data/etc:/etc/gitlab \
        --volume /var/lib/docker/volumes/gitlab-data/log:/var/log/gitlab \
        --volume /var/lib/docker/volumes/gitlab-data/data:/var/opt/gitlab \
        -v /etc/localtime:/etc/localtime \
        --restart=unless-stopped \
        ${imageName}
}

runningCount=`docker ps -f status=running -f status=restarting | grep -w ${serverName} |wc -l`;
if [[ ${runningCount} > 0 ]];then
    echo "docker restart 重启项目:${serverName}"
    docker restart ${serverName}
    exit 0
fi

serverCount=`docker ps -f status=exited -f status=created | grep -w ${serverName} |wc -l`;
# 判断是否已经启动过,且端口为默认端口
if [[ ${serverCount} > 0 ]];then
    if [[ ${serverCount} > 1 ]]; then
        echo "Error : 查找到多个 ${serverName} 容器,请手动启动"
        exit 1
    else
        echo "docker start 启动项目:${serverName}"
        docker start ${serverName}
    fi
else
    name=${imageName%%:*}
    tag=${imageName##*:}
    # 判断是否有该镜像
    imageCount=`docker images | grep -w ${name} | wc -l`;

    if [[ ${imageCount} > 0 ]];then
        echo "docker run 第一次启动项目: ${imageName}"
        runServer
    else
        echo "Error : 还没有该镜像"
        exit 1
    fi
fi

gitlab-docker-restart.sh赋予执行权限:

chmod +x gitlab-docker-restart.sh

之后直接执行 gitlab-docker-restart.sh 即可完成gitlab的重启。

二、配置

# 修改gitlab.rb配置文件
$ vim /var/lib/docker/volumes/gitlab-data/etc/gitlab.rb # 编辑gitlab.rb文件


## GitLab NGINX
nginx['listen_port'] = 80  # gitlab nginx 端口。默认端口为:80 

## GitLab Unicorn
unicorn['listen'] = 'localhost'
unicorn['port'] = 8080 #默认是8080端口

## GitLab URL 配置http协议所使用的访问地址
external_url 'http://song.local' # clone时显示的地址,gitlab 的域名

# 配置ssh协议所使用的访问地址和端口
gitlab_rails['gitlab_ssh_host'] = 'song.local'
gitlab_rails['gitlab_shell_ssh_port'] = 10022
# 退出容器后,重启容器
docker restart gitlab

假如你将gitlab配置文件已经挂载到外部存储路径下,只要在相应目录(如这里的/srv/gitlab/config)下修改gitlab.rb文件,并重启容器就可。

将容器时间和host在一个时区

-v /etc/localtime:/etc/localtime

三、访问

访问 http://127.0.0.1:80,系统默认的管理员账号为 root,第一次访问 GitLab,会要求初始化管理员账号的密码。

查看原文

赞 3 收藏 3 评论 0

Developer 发布了文章 · 2019-04-28

CentOS7搭建GitLab

官方中文教程 https://www.gitlab.com.cn/ins...
Docker 方式安装 GitLab 请见(推荐使用Docker安装):https://segmentfault.com/a/11...

环境要求:内存至少4G,GitLab是很耗内存滴

一、 安装并配置必要的依赖关系

在 CentOS 系统上,下面的命令将会打开系统防火墙 HTTP 和 SSH 的访问。

$ sudo yum install -y curl policycoreutils-python openssh-server
$ sudo systemctl enable sshd
$ sudo systemctl start sshd
$ sudo firewall-cmd --permanent --add-service=http
$ sudo systemctl reload firewalld

安装 Postfix ,用来发送邮件,在安装 Postfix 的过程中选择 'Internet Site'。

$ sudo yum install postfix
$ sudo systemctl enable postfix
$ sudo systemctl start postfix

也可以配置自定义的 SMTP 服务器

二、 添加 GitLab 镜像仓库并安装

gitlab-ce 是社区版,免费
gitlab-ee 是企业版,收费

2.1 使用官方镜像安装

$ curl https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.rpm.sh | sudo bash

$ sudo EXTERNAL_URL="http://gitlab.example.com" yum install -y gitlab-ce # 安装 GitLab

2.2 使用国内镜像安装(推荐)

如果提示连接超时,可以使用 清华大学开源软件镜像站https://mirror.tuna.tsinghua....
进入该网站后,有详细的安装步骤,跟着安装即可。

这里介绍一下在CentOS中使用 清华大学开源软件镜像站安装:
先还原yum源, 删掉gitlab-ce源 :

$ ls -l /etc/yum.repos.d/ # 查看源配置项
$ mv /etc/yum.repos.d/gitlab_gitlab-ce.repo /etc/yum.repos.d/gitlab_gitlab-ce.repo.bak # 备份源配置项(也可以直接删除 rm)

新建 /etc/yum.repos.d/gitlab-ce.repo,内容为

[gitlab-ce]
name=Gitlab CE Repository
baseurl=https://mirrors.tuna.tsinghua.edu.cn/gitlab-ce/yum/el$releasever/
gpgcheck=0
enabled=1

再执行

$ sudo yum makecache
$ sudo yum install gitlab-ce

安装完以后 /opt/gitlab/ 目录结构

/opt/gitlab/
├── backups
├── git-data
│   └── repositories
│       └── root
├── gitlab-ci
│   └── builds
├── gitlab-rails
│   ├── etc
│   ├── shared
│   │   ├── artifacts
│   │   ├── lfs-objects
│   │   └── pages
│   ├── sockets
│   ├── tmp
│   ├── upgrade-status
│   ├── uploads
│   └── working
├── gitlab-shell
├── gitlab-workhorse
├── logrotate
│   └── logrotate.d
├── nginx
│   ├── client_body_temp
│   ├── conf
│   ├── fastcgi_temp
│   ├── logs -> /var/log/gitlab/nginx
│   ├── proxy_cache
│   ├── proxy_temp
│   ├── scgi_temp
│   └── uwsgi_temp
├── postgresql
│   └── data
│       ├── base
│       │   ├── 1
│       │   ├── 12918
│       │   ├── 12923
│       │   └── 16385
│       ├── global
│       ├── pg_clog
│       ├── pg_multixact
│       │   ├── members
│       │   └── offsets
│       ├── pg_notify
│       ├── pg_serial
│       ├── pg_snapshots
│       ├── pg_stat_tmp
│       ├── pg_subtrans
│       ├── pg_tblspc
│       ├── pg_twophase
│       └── pg_xlog
│           └── archive_status
└── redis

三、 配置并启动 GitLab

启动命令

$ sudo gitlab-ctl reconfigure # 首次启动也要用此命令。重新加载配置并启动
$ sudo gitlab-ctl start # 启动
$ sudo gitlab-ctl stop # 停止

/etc/gitlab/ 目录结构:

/etc/gitlab/
├── gitlab.rb
├── gitlab-secrets.json
└── ssl
    └── trusted-certs

gitLab基本配置集中在 /etc/gitlab/gitlab.rb 文件, 每个参数的作用和配置请参考官网的配置说明.
配置参数:

### Advanced settings
# unicorn['listen'] = 'localhost'
# unicorn['port'] = 8090 #默认是8080端口


nginx['listen_port'] = 8081  # gitlab nginx 端口。默认端口为:80 

external_url 'http://192.168.137.129' # clone时显示的地址,gitlab 的域名

配置:

$ sudo gitlab-ctl stop # 先停止 GitLab 服务 
$ vim /etc/gitlab/gitlab.rb # 修改配置文件

进行任何改动后, 保存退出, 在命令行逐个输入以下命令使配置生效

启动数据库, 不然 reconfigure 报错

$ sudo gitlab-ctl restart postgresql
$ sudo gitlab-ctl reconfigure
$ sudo gitlab-ctl start

四、访问

系统默认的管理员账号为 root,第一次访问 GitLab,会要求初始化管理员账号的密码。

五、端口冲突问题

装好后,你会发现一个问题;gitlab 其实是个web;他自带了个nginx;如果你本身也是一台服务器的话,上面也会装有 apache 或者 nginx 等;那么端口就冲突了;

更改gitlab自带nginx的默认端口即可

六、卸载GitLab

$ sudo gitlab-ctl stop # 停止
$ sudo rpm -e gitlab-ce # 卸载
$ ps aux | grep gitlab # 查看守护进程
$ kill -9 18777 # 杀掉守护进程
$ find / -name gitlab | xargs rm -rf # 删除所有包含gitlab的文件
查看原文

赞 5 收藏 4 评论 1

Developer 收藏了文章 · 2019-04-26

分库分表后如何部署上线?

引言

我们先来讲一个段子

面试官:“有并发的经验没?”

应聘者:“有一点。”

面试官:“那你们为了处理并发,做了哪些优化?”

应聘者:“前后端分离啊,限流啊,分库分表啊。。”

面试官:"谈谈分库分表吧?"

应聘者:“bala。bala。bala。。”

面试官心理活动:这个仁兄讲的怎么这么像网上的博客抄的,容我再问问。

面试官:“你们分库分表后,如何部署上线的?”

应聘者:“这!!!!!!”

不要惊讶,写这篇文章前,我特意去网上看了下分库分表的文章,很神奇的是,都在讲怎么进行分库分表,却不说分完以后,怎么部署上线的。这样在面试的时候就比较尴尬了。

你们自己摸着良心想一下,如果你真的做过分库分表,你会不知道如何部署的么?因此我们来学习一下如何部署吧。

ps: 我发现一个很神奇的现象。因为很多公司用的技术比较low,那么一些求职者为了提高自己的竞争力,就会将一些高大上的技术写进自己的low项目中。然后呢,他出去面试害怕碰到从这个公司出来的人,毕竟从这个公司出来的人,一定知道自己以前公司的项目情形。因此为了圆谎,他就会说:“他们从事的是这个公司的老项目改造工作,用了很多新技术进去!”

那么,请你好好思考一下,你们的老系统是如何平滑升级为新系统的!

如何部署

停机部署法

大致思路就是,挂一个公告,半夜停机升级,然后半夜把服务停了,跑数据迁移程序,进行数据迁移。

步骤如下:

(1)出一个公告,比如“今晚00:00~6:00进行停机维护,暂停服务”

(2)写一个迁移程序,读 db-old 数据库,通过中间件写入新库 db-new1 和 db-new2 ,具体如下图所示

clipboard.png

(3)校验迁移前后一致性,没问题就切该部分业务到新库。

顺便科普一下,这个中间件。现在流行的分库分表的中间件有两种,一种是 proxy 形式的,例如 mycat ,是需要额外部署一台服务器的。还有一种是 client 形式的,例如当当出的 Sharding-JDBC ,就是一个jar包,使用起来十分轻便。我个人偏向 Sharding-JDBC ,这种方式,无需额外部署,无其他依赖,DBA也无需改变原有的运维方式。

评价:

大家不要觉得这种方法low,我其实一直觉得这种方法可靠性很强。而且我相信各位读者所在的公司一定不是什么很牛逼的互联网公司,如果你们的产品凌晨1点的用户活跃数还有超过1000的,你们握个爪!毕竟不是所有人都在什么电商公司的,大部分产品半夜都没啥流量。所以此方案,并非没有可取之处。

但是此方案有一个缺点, 累! 不止身体累,心也累!你想想看,本来定六点结束,你五点把数据库迁移好,但是不知怎么滴,程序切新库就是有点问题。于是,眼瞅着天就要亮了,赶紧把数据库切回老库。第二个晚上继续这么干,简直是身心俱疲。

ps: 这里教大家一些技巧啊,如果你真的没做过分库分表,又想吹一波,涨一下工资,建议答这个方案。因为这个方案比较low,low到没什么东西可以深挖的,所以答这个方案,比较靠谱。

另外,如果面试官的问题是

你们怎么进行分库分表的?

这个问题问的很泛,所以回答这个问题建议自己主动把分表的策略,以及如何部署的方法讲出来。因为这么答,显得严谨一些。

不过,很多面试官为了卖弄自己的技术,喜欢这么问

分表有哪些策略啊?你们用哪种啊?

ok。。这个问题具体指向了分库分表的某个方向了,你不要主动答如何进行部署的。等面试官问你,你再答。如果面试官没问,在面试最后一个环节,面试官会让你问让几个问题。你就问

你刚才刚好有提到分库分表的相关问题,我们当时部署的时候,先停机。然后半夜迁移数据,然后第二天将流量切到新库,这种方案太累,不知道贵公司有没有什么更好的方案?

那么这种情况下,面试官会有两种回答。第一种,面试官硬着头皮随便扯。第二种,面试官真的做过,据实回答。记住,面试官怎么回答的不重要。重点的是,你这个问题出去,会给面试官一种错觉:"这个小伙子真的做过分库分表。"

如果你担心进去了,真派你去做分库分表怎么办?OK,不要怕。我赌你试用期碰不到这个活。因为能进行分库分表,必定对业务非常熟。还在试用期的你,必定对业务不熟,如果领导给你这种活,我只能说他有一颗大心脏。

ok,指点到这里。面试本来就是一场斗智斗勇的过程,扯远了,回到我们的主题。

双写部署法(一)

这个就是不停机部署法,这里我需要先引进两个概念: 历史数据 和 增量数据 。

假设,我们是对一张叫做 test_tb 的表进行拆分,因为你要进行双写,系统里头和 test_tb表有关的业务之前必定会加入一段双写代码,同时往老库和新库中写,然后进行部署,那么

历史数据:在该次部署前,数据库表 test_tb 的有关数据,我们称之为历史数据。

增量数据:在该次部署后,数据库表 test_tb 的新产生的数据,我们称之为增量数据。

然后迁移流程如下

(1)先计算你要迁移的那张表的 max(主键) 。在迁移过程中,只迁移 db-old 中 test_tb 表里,主键小等于该 max(主键) 的值,也就是所谓的历史数据。

这里有特殊情况,如果你的表用的是uuid,没法求出 max(主键) ,那就以创建时间作为划分历史数据和增量数据的依据。如果你的表用的是uuid,又没有创建时间这个字段,我相信机智的你,一定有办法区分出历史数据和增量数据。

(2)在代码中,与 test_tb 有关的业务,多加一条往消息队列中发消息的代码,将操作的sql发送到消息队列中,至于消息体如何组装,大家自行考虑。 需要注意的是, 只发写请求的sql,只发写请求的sql,只发写请求的sql。重要的事情说三遍!

原因有二:

(1)只有写请求的sql对恢复数据才有用。

(2)系统中,绝大部分的业务需求是读请求,写请求比较少。

注意了,在这个阶段,我们不消费消息队列里的数据。我们只发写请求,消息队列的消息堆积情况不会太严重!

(3)系统上线。另外,写一段迁移程序,迁移 db-old 中 test_tb 表里,主键小于该 max(主键)的数据,也就是所谓的历史数据。

上面步骤(1)~步骤(3)的过程如下

clipboard.png

(3)校验迁移前后一致性,没问题就切该部分业务到新库。

顺便科普一下,这个中间件。现在流行的分库分表的中间件有两种,一种是 proxy 形式的,例如 mycat ,是需要额外部署一台服务器的。还有一种是 client 形式的,例如当当出的 Sharding-JDBC ,就是一个jar包,使用起来十分轻便。我个人偏向 Sharding-JDBC ,这种方式,无需额外部署,无其他依赖,DBA也无需改变原有的运维方式。

评价:

大家不要觉得这种方法low,我其实一直觉得这种方法可靠性很强。而且我相信各位读者所在的公司一定不是什么很牛逼的互联网公司,如果你们的产品凌晨1点的用户活跃数还有超过1000的,你们握个爪!毕竟不是所有人都在什么电商公司的,大部分产品半夜都没啥流量。所以此方案,并非没有可取之处。

但是此方案有一个缺点, 累! 不止身体累,心也累!你想想看,本来定六点结束,你五点把数据库迁移好,但是不知怎么滴,程序切新库就是有点问题。于是,眼瞅着天就要亮了,赶紧把数据库切回老库。第二个晚上继续这么干,简直是身心俱疲。

ps: 这里教大家一些技巧啊,如果你真的没做过分库分表,又想吹一波,涨一下工资,建议答这个方案。因为这个方案比较low,low到没什么东西可以深挖的,所以答这个方案,比较靠谱。

另外,如果面试官的问题是

你们怎么进行分库分表的?

这个问题问的很泛,所以回答这个问题建议自己主动把分表的策略,以及如何部署的方法讲出来。因为这么答,显得严谨一些。

不过,很多面试官为了卖弄自己的技术,喜欢这么问

分表有哪些策略啊?你们用哪种啊?

ok。。这个问题具体指向了分库分表的某个方向了,你不要主动答如何进行部署的。等面试官问你,你再答。如果面试官没问,在面试最后一个环节,面试官会让你问让几个问题。你就问

你刚才刚好有提到分库分表的相关问题,我们当时部署的时候,先停机。然后半夜迁移数据,然后第二天将流量切到新库,这种方案太累,不知道贵公司有没有什么更好的方案?

那么这种情况下,面试官会有两种回答。第一种,面试官硬着头皮随便扯。第二种,面试官真的做过,据实回答。记住,面试官怎么回答的不重要。重点的是,你这个问题出去,会给面试官一种错觉:"这个小伙子真的做过分库分表。"

如果你担心进去了,真派你去做分库分表怎么办?OK,不要怕。我赌你试用期碰不到这个活。因为能进行分库分表,必定对业务非常熟。还在试用期的你,必定对业务不熟,如果领导给你这种活,我只能说他有一颗大心脏。

ok,指点到这里。面试本来就是一场斗智斗勇的过程,扯远了,回到我们的主题。

双写部署法(一)

这个就是不停机部署法,这里我需要先引进两个概念: 历史数据 和 增量数据 。

假设,我们是对一张叫做 test_tb 的表进行拆分,因为你要进行双写,系统里头和 test_tb表有关的业务之前必定会加入一段双写代码,同时往老库和新库中写,然后进行部署,那么

历史数据:在该次部署前,数据库表 test_tb 的有关数据,我们称之为历史数据。

增量数据:在该次部署后,数据库表 test_tb 的新产生的数据,我们称之为增量数据。

然后迁移流程如下

(1)先计算你要迁移的那张表的 max(主键) 。在迁移过程中,只迁移 db-old 中 test_tb 表里,主键小等于该 max(主键) 的值,也就是所谓的历史数据。

这里有特殊情况,如果你的表用的是uuid,没法求出 max(主键) ,那就以创建时间作为划分历史数据和增量数据的依据。如果你的表用的是uuid,又没有创建时间这个字段,我相信机智的你,一定有办法区分出历史数据和增量数据。

(2)在代码中,与 test_tb 有关的业务,多加一条往消息队列中发消息的代码,将操作的sql发送到消息队列中,至于消息体如何组装,大家自行考虑。 需要注意的是, 只发写请求的sql,只发写请求的sql,只发写请求的sql。重要的事情说三遍!

原因有二:

  • (1)只有写请求的sql对恢复数据才有用。
  • (2)系统中,绝大部分的业务需求是读请求,写请求比较少。

注意了,在这个阶段,我们不消费消息队列里的数据。我们只发写请求,消息队列的消息堆积情况不会太严重!

(3)系统上线。另外,写一段迁移程序,迁移 db-old 中 test_tb 表里,主键小于该 max(主键)的数据,也就是所谓的历史数据。

上面步骤(1)~步骤(3)的过程如下

clipboard.png

等到 db-old 中的历史数据迁移完毕,则开始迁移增量数据,也就是在消息队列里的数据。

(4)将迁移程序下线,写一段订阅程序订阅消息队列中的数据

(5)订阅程序将订阅到到数据,通过中间件写入新库

(6)新老库一致性验证,去除代码中的双写代码,将涉及到 test_tb 表的读写操作,指向新库。

上面步骤(4)~步骤(6)的过程如下

clipboard.png

这里大家可能会有一个问题,在步骤(1)~步骤(3),系统对历史数据进行操作,会造成不一致的问题么?

OK,不会。这里我们对 delete 操作和 update 操作做分析,因为只有这两个操作才会造成历史数据变动, insert 进去的数据都是属于增量数据。

(1)对 db-old 的 test_tb 表的历史数据发出 delete 操作,数据还未删除,就被迁移程序给迁走了。此时 delete 操作在消息队列里还有记录,后期订阅程序订阅到该 delete 操作,可以进行删除。

(2)对 db-old 的 test_tb 表的历史数据发出 delete 操作,数据已经删除,迁移程序迁不走该行数据。此时 delete 操作在消息队列里还有记录,后期订阅程序订阅到该 delete 操作,再执行一次 delete ,并不会对一致性有影响。

对 update 的操作类似,不赘述。

双写部署法(二)

上面的方法有一个硬伤,注意我有一句话

(2)在代码中,与test_tb有关的业务,多加一条往消息队列中发消息的代码,将操作的sql发送到消息队列中,至于消息体如何组装,大家自行考虑。

大家想一下,这么做,是不是造成了严重的代码入侵。将非业务代码嵌入业务代码,这么做,后期删代码的时候特别累。

有没什么方法,可以避免这个问题的?

有的,订阅 binlog 日志。关于 binlog 日志,我尽量下周写一篇《研发应该掌握的binlog知识》,这边我就介绍一下作用

记录所有数据库表结构变更(例如CREATE、ALTER TABLE…)以及表数据修改(INSERT、UPDATE、DELETE…)的二进制日志。binlog不会记录SELECT和SHOW这类操作,因为这类操作对据本身并没有修改。

还记得我们在 双写部署法(一) 里介绍的,往消息队列里发的消息,都是写操作的消息。而 binlog 日志记录的也是写操作。所以订阅该日志,也能满足我们的需求。

于是步骤如下

(1)打开binlog日志,系统正常上线就好

(2)还是写一个迁移程序,迁移历史数据。步骤和上面类似,不啰嗦了。

步骤(1)~步骤(2)流程图如下

clipboard.png

(3)写一个订阅程序,订阅binlog(mysql中有 canal 。至于oracle中,大家就随缘自己写吧)。然后将订阅到到数据通过中间件,写入新库。

(4)检验一致性,没问题就切库。

步骤(3)~步骤(4)流程图如下

clipboard.png

怎么验数据一致性

这里大概介绍一下吧,这篇的篇幅太长了,大家心里有底就行。

(1)先验数量是否一致,因为验数量比较快。

至于验具体的字段,有两种方法:

(2.1)有一种方法是,只验关键性的几个字段是否一致。

(2.2)还有一种是 ,一次取50条(不一定50条,具体自己定,我只是举例),然后像拼字符串一样,拼在一起。用md5进行加密,得到一串数值。新库一样如法炮制,也得到一串数值,比较两串数值是否一致。如果一致,继续比较下50条数据。如果发现不一致,用二分法确定不一致的数据在0-25条,还是26条-50条。以此类推,找出不一致的数据,进行记录即可。

合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!

查看原文

认证与成就

  • 获得 314 次点赞
  • 获得 18 枚徽章 获得 1 枚金徽章, 获得 4 枚银徽章, 获得 13 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-03-21
个人主页被 1.6k 人浏览