redis 学习笔记

首先,你必须有一台redis服务器可以使用,如果还没安装,可以参考我的上一篇 redis 的安装使用

使用JAVA API来操作Redis的例子可以在这里找到:RedisUtil.javaRedisDemo.java

部分笔记摘自《Redis 实战》,它总结得很好。

Redis默认16个数据库,并以数字为索引,从0开始到15,可以手工修改这个配置,增加数量。登录的时候,默认为0库。

Redis与其他数据库的对比

高性能键值缓存服务器memcached也经常被拿来与Redis进行比较:这两者都可以用于存储键值映射,彼此的性能也相关无几,但是Redis能够自动以两种不同的方式将数据写入硬盘,并且Redis除了能存储普通的字符串之外,还可以存储其他4种数据结构,而memcached只能存储普通的字符串键。这些不同之处使得Redis可以用于解决更为广泛的问题,并且既可以用作主数据库(primary database)使用,又可以作为其他存储系统的辅助数据库(auxiliary database)使用。

名称 类型 数据存储选项 查询类型 附加功能
Redis 使用内存存储的非关系数据库 字符串、列表、集合、散列表、有序集合 每种数据类型都有自已的专属命令,另外还有批量操作(bulk operation)和不完全(partial)的事务支持 发布与订阅,主从复制(master/slave replication),持久化,脚本(存储过程,stored procedure)
memcached 使用内存存储的键值缓存 键值之间的映射 创建命令、读取命令、更新命令、删除命令以及其他几个命令 为提升性能而设的多线程服务器
Mysql 关系数据库 每个数据库可以包含多个表,每个表可以包含多个行;可以处理多个表的视图(view);支持空间(spatial)和第三方扩展 SELECT、INSERT、UPDATE、DELETE、函数、存储过程 支持ACID性质(需要使用InnoDB),主从复制,主主复制(master/master replication)
PostgreSQL 关系数据库 每个数据库可以包含多个表,每个表可以包含多个行;可以处理多个表的视图(view);支持空间(spatial)和第三方扩展;支持可定制类型 SELECT、INSERT、UPDATE、DELETE、内置函数、自定义存储过程 支持ACID性质,主从复制,由第三方支持的多主复制(multi-master replication)
MongoDB 使用硬盘存储的非关系文档存储 每个数据库可以包含多个表,每个表可以包含多个无schema(schema-less)的BSON文档 创建命令、读取命令、更新命令、删除命令、条件查询命令等 支持 map-reduce 操作,主从复制,分片,空间索引(spatial index)

Redis提供的5种结构

结构类型 结构存储的值 结构的读写能力
STRING 可以是字符串、整数或者浮点数 对整个字符串或者字符串的其中一部分执行操作,对整数和浮点数执行自增或者自减操作
LIST 一个链表,链表上的每个节点都包含了一个字符串 从链表的两端推入或者弹出元素;根据偏移量对链表进行修剪;读取单个或者多个元素;根据值查找或者移除元素
SET 包含字符串的无序收集器,并且被包含的每个字符串都是独一无二、各不相同的 添加、获取、移除单个元素;检查一个元素是否存在于集合中;计算交集、并集、差集;从集合里面随机获取元素
HASH 包含键值对的无序散列表 添加、获取、移除单个键值对;获取所有键值对
ZSET(有序集合) 字符串成员与浮点数分值之间的有序映射,元素的排列顺序由分值的大小决定 添加、获取、删除单个元素;根据分值范围或者成员来获取元素

在实际中最好还是让主服务器只使用50%~65%的内存,留下30%~45%的内存用于执行BGSAVE命令和创建记录写命令的缓冲区。

当所有成员的分值都相同时,有序集合将根据成员的名字来进行排序;而当所有成员的分值都是0的时候,成员将按照字符串的二进制顺序进行排序。

对于大部分数据库来说,插入行操作的执行速度非常快(插入行只会在硬盘文件末尾进行写入)。不过,对表里面的行进行更新却是一个速度相当慢的操作,因为这种更新除了会引起一次随机读(random read)之外,还可能会引起一次随机写(random write)。

使用cookie实现购物车–也就是将整个购物车都存储到cookie里面的做法非常常见,这种做法的一大优点是无须对数据库进行写入就可以实现购物车功能,而缺点则是程序需要重新解析和验证cookie,确保cookie的格式正确,并且包含的商品都是真正可购买的商品。cookie购物车还有一个缺点:因为浏览器每次发送请求都会连cookie一起发送,所以如果购物车cookie的体积比较大,那么请求发送和处理的速度可能会有所降低。

如果用户对一个不存在的键或者一个保存了空串的键执行自增或者自减操作,那么Redis在执行操作时会将这个键的值当作是0来处理。如果用户尝试对一个值无法被解释为整数或者浮点数的字符串键执行自增或者自减操作,那么Redis将向用户返回一个错误。

Redis不支持嵌套结构特性

Redis的基本事务

Redis的基本事务(basic transaction)需要用到MULTI命令和EXEC命令,这种事务可以让一个客户端在不被其他客户端打断的情况下执行多个命令。和关系数据库那种可以在执行的过程中进行回滚(rollback)的事务不同,在Redis里面,被MULTI命令和EXEC命令包围的所有命令会一个接一个地执行,直到所有命令都执行完毕为止。当一个事务执行完毕之后,Redis才会处理其他客户端的命令。
要在Redis里面执行事务,我们首先需要执行MULTI命令,然后输入那些我们想要在事务里面执行的命令,最后再执行EXEC命令。当Redis从一个客户端那里接收到MULTI命令时,Redis会将这个客户端之后发送的所有命令都放入到一个队列里面,直到这个客户端发送EXEC命令为止,然后Redis就会在不被打断的情况下,一个接一个地执行存储在队列里面的命令。从语义上来说,Redis事务在Python客户端上面是由流水线(pipeline)实现的:对连接对象调用pipeline()方法将创建一个事务,在一切正常的情况下,客户端会自动地使用MULTI和EXEC包裹起用户输入的多个命令。此外,为了减少Redis与客户端之间的通信往返次数,提升执行多个命令时的性能,Python的Redis客户端会存储起事务包含的多个命令,然后在事务执行时一次性地将所有命令都发送给Redis。

持久化选项

Redis提供了两种不同的持久化方法来将数据存储到硬盘里面。一种方法叫快照(snapshotting),它可以将存在于某一时刻的所有数据都写入硬盘里面。另一种方法叫只追加文件(append-only file, AOF),它会在执行写命令时,将被执行的写命令复制到硬盘里面。这两种持久化方法既可以同时使用,又可以单独使用,在某些情况下甚至可以两种方法都不使用。
为了防止Redis因为创建子进程而出现停顿,我们可以考虑关闭自动保存,转而通过手动发送BGSAVE或者SAVE来进行持久化。手动发送BGSAVE一样会引起停顿,唯一不同的是用户可以通过手动发送BGSAVE命令来控制停顿出现的时间。另一方面,虽然SAVE会一直阻塞Redis直到快照生成完毕,但是因为它不需要创建子进程,所以就不会像BGSAVE一样因为创建子进程而导致Redis停顿;并且因为没有子进程在争抢资源,所以SAVE创建快照的速度会比BGSAVE创建快照的速度要来得更快一些。

配置AOF持久化机制

修改redis配置文件,增加如下配置。

1
2
3
4
5
6
7
8
9
$ vi {REDIS_HOME}/redis.conf

appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes

然后重启redis即可。可以登录redis,然后使用命令config get *查看配置。

创建快照的办法有以下几种

  1. 客户端可以通过向Redis发送BGSAVE命令来创建一个快照;
  2. 客户端还可以通过向Redis发送SAVE命令来创建一个快照,接到SAVE命令的Redis服务器在快照创建完毕之前将不再响应任何其他命令;
  3. 用户设置save配置选项;
  4. 当Redis通过SHUTDOWN命令接收到关闭服务器的请求时,或者接收到标准TERM信号时,会执行一个SAVE命令;
  5. 当一个Redis服务器连接另一个Redis服务器,并向对方发送SYNC命令来开始一次复制操作的时候,如果主服务器目前没有在执行BGSAVE操作,或者主服务器并非刚刚执行完BGSAVE操作,那么主服务器就会执行BGSAVE命令。

在只使用快照持久化来保存数据时,一定要记住:如果系统真的发生崩溃,用户将丢失最近一次生成快照之后更改的所有数据。因此,快照持久化只适用于那些即使丢失一部分数据也不会造成问题的应用程序。

当Redis存储的数据量只有几个GB的时候,使用快照来保存数据是没有问题的。Redis会创建子进程并将数据保存到硬盘里面,生成快照所需的时间比你读这句话所需的时间还要短。但随着Redis占用的内存越来越多,BGSAVE在创建子进程时耗费的时间也会越来越多。如果Redis的内存占用量达到数十个GB,并且剩余的空闲内存并不多,或者Redis运行在虚拟机上面,那么执行BGSAVE可能会导致系统长时间地停顿,也可能引发系统大量地使用虚拟内存,从而导致Redis的性能降低至无法使用的程度。

AOF持久化会将被执行的写命令写到AOF文件的末尾,以此来记录数据发生的变化。因此,Redis只要从头到尾重新执行一次AOF文件包含的所有写命令,就可以恢复AOF文件所记录的数据集。通过appendonly yes配置选项来打开。

Redis每秒同步一次AOF文件时的性能和不使用任何持久化特性时的性能相关无几,而通过每秒同步一次AOF文件,Redis可以保证,即使出现系统崩溃,用户也最多只会丢失一秒之内产生的数据。

因为Redis会不断地将被执行的写命令记录到AOF文件里面,所以随着Redis不断运行,AOF文件的体积也会不断增长,在极端情况下,体积不断增大的AOF文件甚至可能会用完硬盘的所有可用空间。还有另一个问题就是,因为Redis在重启之后需要通过重新执行AOF文件记录的所有写命令来还原数据集,所以如果AOF文件的体积非常大,那么还原操作执行的时间就可能会非常长。为了解决AOF文件体积不断增大的问题,用户可以向Redis发送BGREWRITEAOF命令,这个命令会通过移除AOF文件中的冗余命令来重写(rewrite)AOF文件,使AOF文件的体积变得尽可能地小。

关系数据库通常会使用一个主服务器(master)向多个从服务器(slave)发送更新,并使用从服务器来处理所有读请求。在Redis中开启从服务器所必须的选项只有slaveof一个。通过向从服务器发送SLAVEOF no one命令,我们可以让这个从服务器断开与主服务器的连接。因为Redis的主服务器和从服务器并没有特别不同的地方,所以从服务器也可以拥有自已的从服务器,并由此形成主从链(master/slave chaining),如下图:

解决从服务器重同步(resync)问题的其中一个方法,就是减少主服务器需要传送给从服务器的数据数量,这可以通过构建像上图所示的树状复制中间层来完成。除了构建树状的从服务器群组之外,解决从服务器重同步问题的另一个方法就是对网络连接进行压缩,从而减少需要传送的数据量。一些Redis用户就发现使用带压缩的SSH隧道(tunnel)进行连接可以明显地降低带宽占用,如果使用这个方法,记得使用SSH提供的选项来让SSH连接在断线后自动进行连接。

提升Redis读取能力的最简单方法,就是添加只读从服务器。在使用只读从服务器的时候,请务必记得只对Redis主服务器进行写入。在默认情况下,尝试对一个被配置为从服务器的Redis服务器进行写入将引发一个错误(就算这个从服务器是其他从服务器的主服务器,也是如此)。不过,可以通过设置配置选项使从服务器也能执行写入操作,不过由于这一功能通常都处于关闭状态,所以对从服务器进行写入一般都会引发错误。使用多个Redis从服务器处理读查询时可能会遇到的最棘手的问题,就是主服务器临时下线或者永久下线。

Redis Sentinel可以配合Redis的复制功能使用,并对下线的主服务器进行故障转移。

分布式锁

一般来说,在对数据进行“加锁”时,程序首先需要通过获取(acquire)锁来得到对数据进行排他性访问的能力,然后才能对数据进行一系列的操作,最后还要将锁释放(release)给其他程序。对于能够被多个线程访问的共享内存数据结构来说,这种“先获取锁,然后执行操作,最后释放锁”的动作非常常见。Redis使用WATCH命令来代替对数据进行加锁,因为WATCH只会在数据被其他客户端抢先修改了的情况下通知执行了这个命令的客户端,而不会阻止其他客户端对数据进行修改,所以这个命令被称为乐观锁(optimistic locking)。

分布式锁也有类似的“首先获取锁,然后执行操作,最后释放锁”动作,但这种锁既不是给同一个进程中的多个线程使用,也不是给同一台机器上的多个进程使用,而是由不同机器上的不同Redis客户端进行获取和释放锁的。

我们没有直接使用操作系统级别的锁、编程语言级别的锁,或者其他各式各样的锁,而是选择了花费大量时间去使用Redis构建锁,这其中一个原因和范围有关:为了对Redis存储的数据进行排他性访问,客户端需要访问一个锁,这个锁必须定义在一个可以让所有客户端都看得见的范围之内,而这个范围就是Redis本身,因此我们需要把锁构建在Redis里面。另一方面,虽然Redis提供SETNX命令确实具有基本的加锁功能,但它的功能并不完整,并且也不具备分布式锁常见的一些高级特性,所以我们还是需要自已动手来构建分布式锁。

WATCH、MULTI和EXEC组成的事务并不具有可扩展性,原因在于程序在尝试完成一个事务的时候,可能会因为事务执行失败而反复地进行重试。保证数据的正确性是一件非常重要的事情,但使用WATCH命令的做法并不完美 。为了解决这个问题,我们将使用锁。

因为客户端即使在使用锁的过程中也可能会因为这样或那样的原因而下线,所以为了防止客户端在取得锁之后崩溃,并导致锁一直处于“已被获取”的状态,最终版的锁实现将带有超时限制特性:如果获得锁的进程未能在指定的时限内完成操作,那么锁将自动被释放。下面列出了一些导致锁出现不正确实行为的原因,也及锁在不正确运行时的症状:

  1. 持有锁的进程因为操作时间过长而导致锁被自动释放,但进程本身并不知晓这一点,甚至还可能会错误地释放掉了其他进程持有的锁;
  2. 一个持有锁并打算执行长时间操作的进程已经崩溃,但其他想要获取锁的进程不知道哪个进程持有着锁,也无法检测出持有锁的进程已经崩溃,只能白白地浪费时间等待锁被释放;
  3. 在一个进程持有的锁过期之后,其他多个进程同时尝试去获取锁,并且都获得了锁;
  4. 上面提到的第一种情况和第三种情况同时出现,导致有多个进程获得了锁,而每个进程都以为自已是唯一一个获得锁的进程。

在高负载情况下,使用锁可以减少重试次数、降低延迟时间、提升性能并将加锁的粒度调整至合适的大小。

一般来说,当程序使用一个来自Redis的值去构建另一个将要被添加到Redis里面的值时,就需要使用锁或者由WATCH、MULTI和EXEC组成的事务来消除竞争条件。

计数信号量

计数信号量是一种锁,它可以让用户限制一项资源最多能够同时被多少个进程访问,通常用于限定能够同时使用的资源数量。你可以把我们在前一节创建的锁看作是只能被一个进程访问的信号量。计数信号量和其他锁的区别在于,当客户端获取锁失败的时候,客户端通常会选择进行等待;而当客户端获取计数信号量失败的时候,客户端通常会选择立即返回失败结果。

以下是之前介绍过的各个信号量实现的优缺点:

  1. 如果你对于使用系统时钟没有意见,也不需要对信号量进行刷新,并且能够接受信号量的数量偶尔超过限制,那么可以使用我们给出的第一个信号量实现;
  2. 如果你只信任差距在一两秒之间的系统时钟,但仍然能够接受信号量的数量偶尔超过限制,那么你可以使用第二个信号量实现;
  3. 如果你希望信号量一直都具有正确的行为,那么可以使用带锁的信号量实现来保证正确性。

消息拉取

两个或多个客户端在互相发送和接收消息的时候,通常会使用以下两种方法来传递消息。第一种被称为消息推送(push messaging),也就是由发送者来确保所有接收者已经成功接收到了消息。Redis内置了用于进行消息推送的PUBLISH命令和SUBSCRIBE命令。这两个命令有个缺陷:客户端必须一直在线才能接收到消息,断线可能会导致客户端丢失消息。第二种方法被称为消息拉取(pull messaging),这种方法要求接收者自已去获取存储在某种邮箱(mailbox)里面的消息。

索引相关

从文档里面提取单词的过程通常被称为语法分析(parsing)和标记化(tokenization),这个过程可以产生出一系列用于标识文档的标记(token),标记有时候又被称为单词(word)。标记化的一个常见的附加步骤,就是移除内容中的非用词(stop word)。非用词就是那些在文档中频繁出现但是却没有提供相应信息量的单词,对这些单词进行搜索将返回大量无用的结果。移除非用词不仅可以提高搜索性能,还可以减少索引的体积。
用户有些时候可能会想要使用多个具有相同意思的单词进行搜索,并把它们看作是同一个单词,我们把这样的单词称为同义词。
搜索程序在取得多个文档之后,通常还需要根据每个文档的重要性对它们进行排序–搜索领域把这一问题称为关联度计算问题。

广告相关

广告索引操作的特别之处在于它返回的不是一组广告或者一组搜索结果,而是单个广告;并且被索引的广告通常都拥有像位置、年龄或者性别这类必须的定向参数。

Web页面上展示的广告主要有3种类型:按展示次数计费(cost per view)、按点击次数计费(cost per click)和按动作执行次数计费(cost per action)。按展示次数计费的广告又称为CPM广告或按千次计费(cost per mile)广告,这种广告每展示1000次就需要收取固定的费用。按点击计费的广告又称为CPC广告,这种广告根据被点击的次数收取固定的费用。按动作执行次数计费的广告又称为CPA广告,这种广告根据用户在广告的目的地网站上执行的动作收取不同的费用。

让广告的价格保持一致:为了尽可能地简化广告价格的计算方式,程序将对所有类型的广告进行转换,使得它们的价格可以基于每千次展示进行计算,产生出一个估算CPM(estimated CPM),简称eCPM。对于CPM广告来说,因为这种广告已经给出了CPM价格,所以程序只要直接把它的CPM用作eCPM就可以了。至于CPC广告和CPA广告,程序则需要根据相应的规则为它们计算出eCPM。

优化Redis

降低Redis的内存占用有助于减少创建快照和加载快照所需的时间、提升载入AOF文件和重写AOF文件时的效率、缩短从服务器进行同步所需的时间,并且能让Redis存储更多的数据而无需添加额外的硬件。

在列表、散列和有序集合的长度较短或者体积较小的时候,Redis可以选择使用一种名为压缩列表(ziplist)的紧凑存储方式来存储这些结构。压缩列表会以序列化的方式存储数据,这些序列化数据每次被读取的时候都要进行解码,每次被写入的时候也要进行局部的重新编码,并且可能需要对内存里面的数据进行移动。

不同结构关于使用压缩列表表示的配置选项(我安装的是 4.0.11 版本),配置文件位于{REDIS_HOME}/redis.conf

# Hashes are encoded using a memory efficient data structure when they have a
# small number of entries, and the biggest entry does not exceed a given
# threshold. These thresholds can be configured using the following directives.
hash-max-ziplist-entries 512
hash-max-ziplist-value 64

# Lists are also encoded in a special way to save a lot of space.
# The number of entries allowed per internal list node can be specified
# as a fixed maximum size or a maximum number of elements.
# For a fixed maximum size, use -5 through -1, meaning:
# -5: max size: 64 Kb  <-- not recommended for normal workloads
# -4: max size: 32 Kb  <-- not recommended
# -3: max size: 16 Kb  <-- probably not recommended
# -2: max size: 8 Kb   <-- good
# -1: max size: 4 Kb   <-- good
# Positive numbers mean store up to _exactly_ that number of elements
# per list node.
# The highest performing option is usually -2 (8 Kb size) or -1 (4 Kb size),
# but if your use case is unique, adjust the settings as necessary.
list-max-ziplist-size -2

# Sets have a special encoding in just one case: when a set is composed
# of just strings that happen to be integers in radix 10 in the range
# of 64 bit signed integers.
# The following configuration setting sets the limit in the size of the
# set in order to use this special memory saving encoding.
set-max-intset-entries 512

# Similarly to hashes and lists, sorted sets are also specially encoded in
# order to save a lot of space. This encoding is only used when the length and
# elements of a sorted set are below the following limits:
zset-max-ziplist-entries 128
zset-max-ziplist-value 64

说明:

  1. entries选项说明散列、集合和有序集合在被编码为压缩列表的情况下,允许包含的最大元素数量;
  2. value选项则说明了压缩列表每个节点的最大体积是多少个字节;
  3. 当上述两个选项的限制条件中的任意一个被突破的时候,Redis就会将相应的列表、散列或是有序集合从压缩列表编码转换为其他结构,而内存占用也会因此而增加;
  4. 当压缩列表被转换为普通的结构之后,即使结构将来重新满足配置选项设置的限制条件,结构也不会重新转换回压缩列表;
  5. 如果整数包含的所有成员都可以被解释为十进制整数,而这些整数又处于平台的有符号整数范围之内,并且集合成员的数量又足够少的话(上面有配置),那么Redis就会以有序整数数组的方式存储集合,这种存储方式又被称为整数集合(intset)。以有序数组的方式存储集合不仅可以降低内存消耗,还可以提升所有标准集合的执行速度。

缺点:读写一个长度较大的压缩列表可能会给性能带来负面的影响,随着紧凑结构的体积变得越来越大,操作这些结构的速度也会变得越来越慢。

让键名保持简短

到目前为止尚未提到的一件事,就是减少键长度的作用,这里所说的“键”包括所有数据库键、散列的域、集合和有序集合的成员以及所有列表的节点,键的长度越长,Redis需要存储的数据也就越多。

分片结构

分片本质上就是基于某些简单的规则将数据划分为更小的部分,然后根据数据所属的部分来决定将数据发送到哪个位置上面。

使用Lua来扩展Redis

使用Lua编程语言进行的服务器端脚本编程功能,这个功能可以让用户直接在Redis内部执行各种操作,从而达到简化代码并提高性能的作用。将脚本载入Redis需要用到一个名为SCRIPT LOAD的命令,这个命令接受一个字符串格式的Lua脚本为参数,它会把脚本存储起来等待之后使用,然后返回被存储脚本的SHA1校验和。之后,用户只要调用EVALSHA命令,并输入脚本的SHA1校验和以及脚本所需的全部参数就可以调用之前存储的脚本。

Lua版本的锁实现减少了加锁时所需的通信往返次数,所以Lua版本的锁实现在尝试获取锁时的速度比原版的锁要快得多。虽然Lua脚本可以提供巨大的性能优势,并且能在一些情况下大幅地简化代码,但是我们也要记住,运行在Redis内部的Lua脚本只能访问位于Lua脚本之内或者Redis数据库之内的数据,而锁或WATCH/MULTI/EXEC事务并没有这一限制。

Redis在将数据库持久化到硬盘的时候,需要用到fork系统调用,而Windows并不支持这个调用。在缺少fork调用的情况下,Redis在执行持久化操作期间就只能够阻塞所有客户端,直到持久化操作执行完毕为止。

查看一个对象的类型可以使用DEBUG OBJECT命令

127.0.0.1:6379> rpush test a b c d
(integer) 4
127.0.0.1:6379> DEBUG OBJECT test
Value at:0x7f6160c774e0 refcount:1 encoding:quicklist serializedlength:25 lru:9577083 lru_seconds_idle:44 ql_nodes:1 ql_avg_node:4.00 ql_ziplist_max:-2 ql_compressed:0 ql_uncompressed_size:23
127.0.0.1:6379>

redis命令行查看中文显示乱码

Redis在使用命令行操作时,如果查看内容中包含中文,会显示16进制的字符串\xe4\xbf\xa1\xe9\x98\xb3\xe5\xb8\x82\,为了正确显示中文,在启动客启端的时候,加上--raw参数即可:

1
2
3
$ ./redis-cli -h 127.0.0.1 --raw
127.0.0.1:6379> get city
广州

redis 主从配置

要配置主从,我们必须安装两台redis:主服务器为master,从服务器为slave。安装步骤请参考我的上一篇 redis 的安装使用

我们在一台机器上面配置主从,多台的配置是一样的,只要修改下IPPORT即可。以我们上一篇安装好的一台redis为基础,它的安装路径为:/home/hewentian/ProjectD/redis-4.0.11

1
2
3
$ cd /home/hewentian/ProjectD
$ mv redis-4.0.11 redis-4.0.11_master
$ cp -r redis-4.0.11_master/ redis-4.0.11_slave/

master服务器为:/home/hewentian/ProjectD/redis-4.0.11_master
slave服务器为:/home/hewentian/ProjectD/redis-4.0.11_slave

master服务器使用默认端口6379,所以不用配置。下面我们配置slave服务器:

1
2
3
4
5
6
7
$ cd /home/hewentian/ProjectD/redis-4.0.11_slave/
$ vi redis.conf

// 将默认端口改为6380,并加上 slaveof 127.0.0.1 6379 和 pidfile,在文件里面改动如下:
port 6380
slaveof 127.0.0.1 6379
pidfile /var/run/redis_6380.pid

保存文件并退出,slave服务器配置完成。

接着我们启动masterslave服务器

启动master服务器

1
2
$ cd /home/hewentian/ProjectD/redis-4.0.11_master/src/
$ ./redis-server /home/hewentian/ProjectD/redis-4.0.11_master/redis.conf

启动slave服务器

1
2
$ cd /home/hewentian/ProjectD/redis-4.0.11_slave/src/
$ ./redis-server /home/hewentian/ProjectD/redis-4.0.11_slave/redis.conf

master服务器窗口中,可以看到如下信息:

1514:M 14 Aug 15:58:33.568 * Slave 127.0.0.1:6380 asks for synchronization
1514:M 14 Aug 15:58:33.568 * Full resync requested by slave 127.0.0.1:6380
1514:M 14 Aug 15:58:33.568 * Starting BGSAVE for SYNC with target: disk
1514:M 14 Aug 15:58:33.569 * Background saving started by pid 1584
1584:C 14 Aug 15:58:33.573 * DB saved on disk
1584:C 14 Aug 15:58:33.573 * RDB: 0 MB of memory used by copy-on-write
1514:M 14 Aug 15:58:33.576 * Background saving terminated with success
1514:M 14 Aug 15:58:33.576 * Synchronization with slave 127.0.0.1:6380 succeeded

slave服务器窗口中,可以看到如下信息:

1579:S 14 Aug 15:58:33.567 * Connecting to MASTER 127.0.0.1:6379
1579:S 14 Aug 15:58:33.567 * MASTER <-> SLAVE sync started
1579:S 14 Aug 15:58:33.568 * Non blocking connect for SYNC fired the event.
1579:S 14 Aug 15:58:33.568 * Master replied to PING, replication can continue...
1579:S 14 Aug 15:58:33.568 * Partial resynchronization not possible (no cached master)
1579:S 14 Aug 15:58:33.569 * Full resync from master: 7f883c61e9326b040b150462901e70afd5c4a49c:0
1579:S 14 Aug 15:58:33.576 * MASTER <-> SLAVE sync: receiving 176 bytes from master
1579:S 14 Aug 15:58:33.577 * MASTER <-> SLAVE sync: Flushing old data
1579:S 14 Aug 15:58:33.577 * MASTER <-> SLAVE sync: Loading DB in memory
1579:S 14 Aug 15:58:33.577 * MASTER <-> SLAVE sync: Finished with success

从上面的信息中,我们可以知道,slave服务器已经连上master服务器。

测试同步数据

登录master服务器并在其中插入一个键

1
2
3
4
5
6
7
8
9
10
$ cd /home/hewentian/ProjectD/redis-4.0.11_master/src/
$ ./redis-cli -p 6379

127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> set name "Tim Ho"
OK
127.0.0.1:6379> get name
"Tim Ho"
127.0.0.1:6379>

登录slave服务器并在其中获取一个键

1
2
3
4
5
6
7
8
9
10
$ cd /home/hewentian/ProjectD/redis-4.0.11_slave/src/
$ ./redis-cli -p 6380

127.0.0.1:6380> keys *
(empty list or set)
127.0.0.1:6380> keys *
1) "name"
127.0.0.1:6380> get name
"Tim Ho"
127.0.0.1:6380>

至此,主从配置完成。

redis 的安装使用

要使用redis我们首先要安装这个软件。下面,我们说说redis的安装过程:

首先,我们要将redis安装包下载回来,截止本文写时,redis官网发布的最新版本为4.0.11。当然,我们也可以从这里下载 redis-4.0.11.tar.gzredis-4.0.11.tar.gz.md5。推荐从redis官网下载最新版本。

1
2
3
4
5
6
7
8
9
10
$ cd /home/hewentian/ProjectD/
$ wget http://download.redis.io/releases/redis-4.0.11.tar.gz

// 验证下载文件的完整性,在下载的时候要将MD5文件或者SHA256文件也下载回来
$ md5sum -c redis-4.0.11.tar.gz.md5
redis-4.0.11.tar.gz: OK

$ tar xzf redis-4.0.11.tar.gz
$ cd redis-4.0.11/
$ make

After building Redis, it is a good idea to test it using:

1
2
3
4
$ make test

// 如果见到下面的结果,证明我们已经成功安装
\o/ All tests passed without errors!

下面的内容摘自${REDIS_HOME}/README.md

Running Redis

To run Redis with the default configuration just type:

$ cd src
$ ./redis-server

If you want to provide your redis.conf, you have to run it using an additional
parameter (the path of the configuration file):

$ cd src
$ ./redis-server /path/to/redis.conf

It is possible to alter the Redis configuration by passing parameters directly
as options using the command line. Examples:

$ ./redis-server --port 9999 --slaveof 127.0.0.1 6379
$ ./redis-server /etc/redis/6379.conf --loglevel debug

All the options in redis.conf are also supported as options using the command
line, with exactly the same name.

Playing with Redis

You can use redis-cli to play with Redis. Start a redis-server instance,
then in another terminal try the following:

$ cd src
$ ./redis-cli    # 如果需要连接到指定的redis可以使用:-h {IP_ADDRESS}参数,如:./redis-cli -h 127.0.0.1
redis> ping
PONG
redis> set foo bar
OK
redis> get foo
"bar"
redis> incr mycounter
(integer) 1
redis> incr mycounter
(integer) 2
redis>

You can find the list of all the available commands at http://redis.io/commands.

注意:启动Redis服务器的时候,请务必指定它的配置文件,否则有可能会出现意想不到的情况。

为Redis设置登录密码

为Redis设置登录密码的方法比较简单,打开${REDIS_HOME}/redis.conf文件,找到requirepass foobared这一行,如下:

################################## SECURITY ###################################

# Require clients to issue AUTH <PASSWORD> before processing any other
# commands.  This might be useful in environments in which you do not trust
# others with access to the host running redis-server.
#
# This should stay commented out for backward compatibility and because most
# people do not need auth (e.g. they run their own servers).
#
# Warning: since Redis is pretty fast an outside user can try up to
# 150k passwords per second against a good box. This means that you should
# use a very strong password otherwise it will be very easy to break.
#
# requirepass foobared

requirepass foobared的注释打开,并把foobared设置成自己的密码即可,如将其设为abc123:

requirepass abc123

保存文件,并重启Redis。

在使用客户端登录的时候,将使用如下命令:

$ cd src
$ ./redis-cli -a abc123

或者使用下面的方式更安全
$ cd src
$ ./redis-cli
127.0.0.1:6379> AUTH abc123
OK

一些常规配置

1
2
3
4
5
6
7
$ vi {REDIS_HOME}/redis.conf

pidfile /home/hewentian/db/redis/redis.pid
logfile /home/hewentian/db/redis/logs/redis.log
dir /home/hewentian/db/redis/data
daemonize yes # 是否以后台进程运行
bind 127.0.0.1 redis.hewentian.com # 127.0.0.1只能在本机访问,如果要在外网访问,要配置多一个地址,用空格隔开

jvm 学习笔记

最近在看周志明先生的《深入理解JAVA虚拟机JVM高级特性与最佳实践》,作笔记如下,以便自已复习。文中代码,大部分摘自书中。

JVM的基本结构:类加载子系统、方法区、JAVA堆、JAVA栈、本地方法区、程序计数器、直接内存、垃圾回收系统和执行引擎。

JVM的运行时数据区如下所示:

JAVA的NIO库允许程序使用直接内存,从而提高性能,通常直接内存速度会优于堆。读写频繁的场合,可以优先考虑使用。

lambda函数式编程的一个重要优点就是这样的程序天然地适合并行运行,这对JAVA语言在多核时代继续保持主流语言的地位有很大的帮助。

垃圾回收算法

  1. 引用计数法:是古老而经典的垃圾收集算法,其核心就是在对象被其他对象所引用的时候计数器加1,而当引用失效时则减1。但是这种方式有非常严重的问题:无法处理循环引用的情况、还有的就是每次进行加减操作比较浪费系统性能。
  2. 标记清除法:分为标记和清除两个阶段,这种方式也有非常大的漏洞弊端,就是空间碎片问题,垃圾回收后,空间不是连续的。
  3. 复制算法:其核心思想就是将内存分为两块,每次只使用其中一块。在回收时,将正在使用的内存中的存留对象复制到未被使用的内存中去,之后清除之前正在使用的内存中的所有对象,反复去交换两个内存的角式。
  4. 标记压缩法:是在标记清除法的基础上做了优化,把存活的对象压缩到内存的一端,然后进行垃圾清理(老年代中使用的回收方法)
  5. 分代算法:根据对象的特点把内存分成N块,然后根据每个对象的特点使用不同的算法。对于新生代,它的回收频率很高,但是每次回收耗时都很短;而老年代回收频率较低,但是耗时相对较长,所以应该尽量减少老年代的GC。

确定对象是否已死的方法

也就是如何判定对象是否为垃圾。

  1. 引用计数法;
  2. 可达性分析算法GC Root.

GC Root有以下几种:

  1. jvm stack
  2. native method stack
  3. runtime constant pool
  4. static references in method area

翻译如下:

  1. 虚拟机栈中(局部变量)的引用对象
  2. 本地方法栈中JNI(Native方法)的引用对象
  3. 方法区中常量引用的对象(final 的常量值)
  4. 方法区中的类静态属性引用对象

垃圾回收器

  1. 串行回收器: 使用单线程进行垃圾回收的回收器。每次回收时,串行回收器只有一个工作线程,对于并行能力较弱的计算机来说,串行回收器的专注性和独占性往往有更好的性能表现。新生代采用复制算法,老年代采用标记-整理算法,它在收集的同时,所有的用户线程必须暂停(Stop The World)。可以在新生代和老年代使用。新生代,-XX:+UseSerialGC。老年代,-XX:+UseSerialOldGC
  2. 并行回收器: 在串行回收器的基础上作了改进,他可以使用多个线程同时进行垃圾回收,对于计算能力强的计算机而言,可以有效的缩短回收所需的实际时间。他只是简单的将串行回收器多线程化,他的回收策略和算法和串行回收器一样,同样采用复制算法。只使用在新生代。-XX:+UseParNewGC
  3. Parallel Scavenge: 新生代回收器,使用了复杂算法的收集器,也是多线程独占形式的收集器,它有个特点,就是它非常关注系统的吞吐量。适用场景:注重吞吐量,高效利用CPU,需要高效运算且不需要太多交互。可以使用-XX:+UseParallelGC来选择Parallel Scavenge作为新生代收集器,jdk7、jdk8默认使用它作为新生代收集器。
  4. CMS:全称为Concurrent Mark Sweep,意为并发标记清除,他使用的是标记清除法,主要关注系统停顿时间。-XX:+UseConcMarkSweepGC进行设置,-XX:ConcGCThreads设置并发线程数量。CMS并不是独占的回收器,也就是说CMS回收的过程中,应用程序仍然可以在不停的工作。无法处理浮动垃圾:在并发清理阶段,由于用户线程还在运行,还会不断产生新的垃圾,CMS收集器无法在当次收集中清除这部分垃圾。CMS比较耗内存,CMS不会等到应用程序饱和的时候才去回收垃圾,而是在某一个阀值的时候开始回收,可以使用参数指定:-XX:CMSInitiatingOccupancyFraction来指定,默认为68,也就是说当老年代的空间使用率达68%的时候,会执行CMS回收。如果内存不足,收集可能会失败,如果失败了,会启动老年代串行回收器。CMS收集器是一种以最短回收停顿时间为目标的收集器,以“最短用户线程停顿时间”著称。整个垃圾收集过程分为4个步骤:(1)初始标记:标记一下GC Roots能直接关联到的对象,速度较快。(2)并发标记:进行GC Roots Tracing,标记出全部的垃圾对象,耗时较长。(3)重新标记:修正并发标记阶段因用户程序继续运行而导致变化的对象的标记记录,耗时较短。(4)并发清除:用标记-清除算法清除垃圾对象,耗时较长。整个过程耗时最长的并发标记和并发清除都是和用户线程一起工作,所以从总体上来说,CMS收集器垃圾收集可以看做是和用户线程并发执行的。适用场景:重视服务器响应速度,要求系统停顿时间最短。
  5. G1:Garbage-First是在JDK1.7中提出的垃圾回收器,是为了取代CMS的回收器。它属于分代回收器,并行性和并发性。并行性是G1回收期间可多线程同时工作,而并发性是G1拥有与应用程序交替执行能力,部分工作可与应用程序同时执行,在整个GC期间不会完全阻塞应用程序。它可以工作在新生代和老年代。之前的回收器,或者工作在新生代,或者工作在老年代。G1使用了有益智复制对象的方式,减少空间碎片。G1收集器收集器收集过程有初始标记、并发标记、最终标记、筛选回收,和CMS收集器前几步的收集过程很相似:(1)初始标记:标记出GC Roots直接关联的对象,这个阶段速度较快,需要停止用户线程,单线程执行。(2)并发标记:从GC Root开始对堆中的对象进行可达性分析,找出存活对象,这个阶段耗时较长,但可以和用户线程并发执行。(3)最终标记:修正在并发标记阶段因用户程序执行而产生变动的标记记录。(4)筛选回收:筛选回收阶段会对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来指定回收计划(用最少的时间来回收包含垃圾最多的区域,这就是Garbage First的由来——第一时间清理垃圾最多的区块),这里为了提高回收效率,并没有采用和用户线程并发执行的方式,而是停顿用户线程。适用场景:要求尽可能可控GC停顿时间;内存占用较大的应用。可以用-XX:+UseG1GC使用G1收集器,JDK9默认使用G1收集器。

一些参数:
-XX:+UseSerialGC:在新生代和老年代使用串行收集器
-XX:+UseParNewGC:在新生代使用并行收集器
-XX:+UseParallelGC:新生代使用并行回收收集器,更加关注吞吐量
-XX:+UseParallelOldGC:老年代使用并行回收收集器
-XX:ParallelGCThreads:设置用于垃圾回收的线程数
-XX:+UseConcMarkSweepGC:新生代使用并行收集器,老年代使用CMS+串行收集器
-XX:ParallelCMSThreads:设定CMS的线程数量
-XX:+UseG1GC:启用G1垃圾回收器

将GC的日志输出到文件可以配置JVM启动参数:-Xloggc:/home/hewentian/Document/gc.log

minorGC:Eden区满的时候执行;
FullGC:老年代空间不足时,或方法区空间不足时执行,System.gc()

class文件格式

class文件是一组以8位字节码为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在class文件之中,中间没有添加任何分隔符,这使得整个class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。

无符号数属于基本的数据类型,以u1, u2, u4, u8来分别代表1,2,4,8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。

如下图所示:

魔数与class文件的版本

魔数: 每个class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的class文件。
版本号: 紧接着魔数的4个字节存储的是class文件的版本号:第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。只有当前JVM的版本号大于等于这个文件的版本号,该文件才会被JVM加载。

类型转换

JVM直接支持(转换时无需显式的转换指令):

  1. int类型到long, float或者double类型;
  2. long类型到float, double类型;
  3. float类型到double类型。

JVM中对象的创建过程

  1. 当使用创建指令new一个对象的时候,在方法区的常量池中定位一个对象的符号引用,如果该类未被加载就先加载;
  2. 然后再为该对象分配内存,最后是对象初始化,即调用它的构造方法。分配内存分指针碰撞和空闲链表两种。

类的加载过程

JVM中类的加载过程:加载、验证、准备、解析、初始化。如果算上:使用和缷载这两个过程,则一共有7个过程。

加载:加载要完成以下三件事:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流(不一定从文件中读取,可以从网络或数据库中);
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

验证:验证是连接阶段的第一步,这一阶段的目的是为了确保class文件的字节流中包含的信息符合当前JVM的要求,并且不会危害JVM自身的安全。
准备:正式为类变量(static修饰)分配内存并设置类变量初始值的阶段,通常是该类型的零值;

1
public static int value = 123;

变量value在准备阶段过后的初始值为0,而不是123。赋值为123在初始化阶段才会执行。

解析:JVM将常量池内的符号引用替换为直接引用的过程;
初始化:执行类构造器的过程,设置实例变量的初始值。

其中,加载、验证、准备、初始化和缷载这5个阶段的顺序是确定的。如下图所示:

类加载器

从JAVA开发人员的角度来看,绝大部分JAVA程序都会使用到以下3种系统提供的类加载器:

  1. 启动类加载器(Bootstrap ClassLoader):这个类加载器负责将存放在<JAVA_HOME>/lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被JAVA程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那么使用null代替即可。
  2. 扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>/lib/ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
  3. 应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自已的类加载器,一般情况下这个就是程序中默认的类加载器。

我们的应用程序都是由这3种类加载器互相配合进行加载的,如果有必要,还可以加入自已定义的类加载器。这些类加载器之间的关系一般如下图所示,这种层次关系,称为类加载器的双亲委派模型。

什么时候触发类加载:

  1. new一个类实例的时候;
  2. 调用一个类的static变量或方法的时候,例如:System.out
  3. 反射调用的时候,如果该类还没进行过初始化;
  4. 初始化一个类,其父类还没初始化时,会先初始化其父类;
  5. 启动时的类,即运行main方法类。

静态加载和动态加载:
静态加载:在代码中通过new创建实例,称为静态加载;
动态加载:在运行时,通过Class.forName()加载一个类,称为动态加载。

loadClass()Class.forName()的区别:
ClassLoader.loadClass()仅会将类加载到内存中,但不会实例化对象。而Class.forName()在加载之后,会实例化对象,也就是说它会返回一个对象。

newnewInstance()创建类的区别:

  1. newInstance()必须保证这个类被加载;
  2. new关键字,如果类没有被加载,那么就会先加载;
  3. new可以调用类的任何构造方法,而newInstance()只能调用默认的无参构造方法;
  4. new出来的对象是强类型的,效率高;newInstance()创建的对象是弱类型的,效率相对较低。

当泛型遇上重载

下面的代码是不能通过编译的,因为参数List<String>List<Integer>编译之后都被擦除了,变成了一样的List<E>,擦除动作导致这两种方法的特征签名变得一模一样。

1
2
3
4
5
6
7
public static void method(List<String> list) {
System.out.println("invoke method(List<String> list)");
}

public static void method(List<Integer> list) {
System.out.println("invoke method(List<Integer> list)");
}

如果真是要重载,可以适当修改上述代码,只要增加返回值即可,它也只能在JDK1.6上可以编译通过。在JDK1.8也是无法编译通过的。如下所示:

1
2
3
4
5
6
7
8
9
public static String method(List<String> list) {
System.out.println("invoke method(List<String> list)");
return "";
}

public static int method(List<Integer> list) {
System.out.println("invoke method(List<Integer> list)");
return 1;
}

自动装箱的陷阱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) throws Exception {
Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
Long g = 3L;

// 在 JDK1.8 中的结果如下
System.out.println(c == d); // true, [-127, 128]之间的Integer数字会被缓存
System.out.println(e == f); // false
System.out.println(c == (a + b)); // true
System.out.println(c.equals(a + b)); // true
System.out.println(g == (a + b)); // true
System.out.println(g.equals(a + b)); // false
}

final语言校验

下面这两段代码编译出来的class文件是一样的,没有任何区别。只是在编写程序的时候会受到final的约束。

1
2
3
4
5
6
7
8
9
10
11
// 方法一:带有final修饰
public void foo(final int arg) {
final int var = 0;
// do something
}

// 方法二:没有final修饰
public void foo(int arg) {
int var = 0;
// do something
}

解释器与编译器

尽管不是所有的JVM都采用解释器与编译器并存的架构,但许多主流的商用JVM,如HotSpot、J9等,都同时包含解释器与编译器。但是,三大商用JVM之一的JRockit是个例外,它内部没有解释器。解释器与编译器两者各有优势:当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率。当程序运行环境中内存资源限制较大(如部分嵌入式系统中),可以使用解释执行节约内存,反之可以使用编译执行来提升效率。在整个JVM执行架构中,解释器与编译器经常配合工作,如下图。

程序编译与代码优化

从sun javac的代码来看,编译过程大致可以分成3个过程:

  1. 解析与填充符号表的过程(Parse and Enter);
  2. 插入式注解处理器的注解处理过程(Annotation Processing);
  3. 分析与字节码生成的过程(Analyse and Generate)。

编译对象与触发条件

在程序运行的过程中会被即时编译器JIT编译的“热点代码”有两类:

  1. 被多次调用的方法;
  2. 被多次执行的循环体。

判断一段代码是不是热点代码,是不是需要触发即时编译,这样的行为称为热点探测(Hot Spot Detection),其实进行热点探测并不一定要知道方法具体被调用了多少次,目前主要的热点探测判定方法有两种,分别如下:

  • 基于采样的热点探测(Sample Based Hot Spot Detection):采用这种方法的JVM会周期性地检查各个线程的栈顶,如果发现某个(或某些)方法经常出现在栈顶,那这个方法就是“热点方法”。基于采样的热点探测的好处是实现简单、高效,还可以很容易地获取方法调用关系(将调用堆栈展开即可),缺点是很难精确地确认一个方法的热度,容易因为受到线程阻塞或别的外界因素的影响而扰乱热点探测。
  • 基于计数器的热点探测(Counter Based Hot Spot Detection):采用这种方法的JVM会为每个方法(甚至是代码块)建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值就认为它是“热点方法”。这种统计方法实现起来麻烦一些,需要为每个方法建立并维护计数器,而且不能直接获取到方法的调用关系,但是它的统计结果相对来说更加精确和严谨。

在HotSpot虚拟机中使用的是第二种–基于计数器的热点探测方法,因此它为每个方法准备了两类计数器:方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter)。

在确定虚拟机运行参数的前提下,这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发JIT编译。

我们首先来看看方法调用计数器,顾名思义,这个计数器就用于统计方法被调用的次数,它的默认阈值在Client模式下是1500次,在Server模式下是10000次,这个阈值可以通过JVM参数-XX:CompileThreshold来人为设定。

下面我们再来看看回边计数器。什么是回边? 在字节码遇到控制流向后跳转的指令称为回边(Back Edge)。回边计数器是用来统计一个方法中循环体代码执行的次数,回边计数器的阈值可以通过参数-XX:OnStackReplacePercentage来调整。

虚拟机运行在Client模式下,回边计数器阈值计算公式为:
方法调用计数器阈值(CompileThreshold) x OSR比率(OnStackReplacePercentage) / 100
其中OnStackReplacePercentage默认值为933,如果都取默认值,那Client模式虚拟机的回边计数器的阈值为13995.

虚拟机运行在Server模式下,回边计数器阈值的计算公式为:
方法调用计数器阈值(CompileThreshold) x (OSR比率(OnStackReplacePercentage) - 解释器监控比率(InterpreterProfilePercentage) / 100
其中OnStackReplacePercentage默认值为140,InterpreterProfilePercentage默认值为33。
如果都取默认值,那Server模式虚拟机回边计数器的阑值为10700。

回边计数器与方法调用计数器不同的是,回边计数器没有热度衰减,因此这个计数器统计的就是循环执行的绝对次数。

参见下图:

程序延时一定时间

像下面的空循环经JIT编译器优化后,会被消除掉,以前很多入门教程把空循环当做程序延时的手段来介绍,其实是错误的。

1
2
for(int i = 1; i <= 10000; i++)
;

要延时应该使用下面的方法

1
2
3
4
5
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

不使用的对象应手动设为null,不过,赋值为null的操作在经过JIT编译优化后就会被消除掉,这时候将变量设置为null就是没有意义的。

线程、主内存、工作内存、处理器的关系图

除了增加高速缓存,为了使处理器内部运算单元尽可能被充分利用,处理器还会对输入的代码进行乱序执行(Out-Of-Order Execution)优化,处理器会在乱序执行之后的结果进行重组,保证结果的正确性,也就是保证结果与顺序执行的结果一致。但是在真正的执行过程中,代码执行的顺序并不一定按照代码的书写顺序来执行,可能和代码的书写顺序不同。

java内存模型

虽然java程序所有的运行都是在虚拟机中,涉及到的内存等信息都是虚拟机的一部分,但实际也是物理机的,只不过是虚拟机作为最外层的容器统一做了处理。虚拟机的内存模型,以及多线程的场景下与物理机的情况是很相似的,可以类比参考。
Java内存模型的主要目标是定义程序中变量的访问规则。即在虚拟机中将变量存储到主内存或者将变量从主内存取出这样的底层细节。需要注意的是这里的变量跟我们写java程序中的变量不是完全等同的。这里的变量是指实例字段,静态字段,构成数组对象的元素,但是不包括局部变量和方法参数(因为这是线程私有的)。这里可以简单的认为主内存是java虚拟机内存区域中的堆,局部变量和方法参数是在虚拟机栈中定义的。但是在堆中的变量如果在多线程中都使用,就涉及到了堆和不同虚拟机栈中变量的值的一致性问题了。

Java内存模型中涉及到的概念有:

  • 主内存:java虚拟机规定所有的变量(不是程序中的变量)都必须在主内存中产生,为了方便理解,可以认为是堆区。可以与前面说的物理机的主内存相比,只不过物理机的主内存是整个机器的内存,而虚拟机的主内存是虚拟机内存中的一部分。
  • 工作内存:java虚拟机中每个线程都有自己的工作内存,该内存是线程私有的为了方便理解,可以认为是虚拟机栈。可以与前面说的高速缓存相比。线程的工作内存保存了线程需要的变量在主内存中的副本。虚拟机规定,线程对主内存变量的修改必须在线程的工作内存中进行,不能直接读写主内存中的变量。不同的线程之间也不能相互访问对方的工作内存。如果线程之间需要传递变量的值,必须通过主内存来作为中介进行传递。

这里需要说明一下:主内存、工作内存与java内存区域中的java堆、虚拟机栈、方法区并不是一个层次的内存划分。这两者基本上是没有关系的,上文只是为了便于理解,做的类比。

内存间交互操作

关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存复制到工作内存、如何从工作内存同步回主内存之类的实现细节,JAVA内存模型中定义了以下8种操作来完成,虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许有例外)。

  • lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态;
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;
  • read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用;
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中;
  • use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作;
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作;
  • store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用;
  • write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

volatile变量的使用场景

关键字volatile可以说是java虚拟机中提供的最轻量级的同步机制。java内存模型对volatile专门定义了一些特殊的访问规则。这些规则有些晦涩拗口,先列出规则,然后用更加通俗易懂的语言来解释:
假定T表示一个线程,V和W分别表示两个volatile修饰的变量,那么在进行read、load、use、assign、store和write操作的时候需要满足如下规则:

  • 只有当线程T对变量V执行的前一个动作是load,线程T对变量V才能执行use动作;同时只有当线程T对变量V执行的后一个动作是use的时候线程T对变量V才能执行load操作。所以,线程T对变量V的use动作和线程T对变量V的read、load动作相关联,必须是连续一起出现。也就是在线程T的工作内存中,每次使用变量V之前必须从主内存去重新获取最新的值,用于保证线程T能看得见其他线程对变量V的最新的修改后的值。

  • 只有当线程T对变量V执行的前一个动作是assign的时候,线程T对变量V才能执行store动作;同时只有当线程T对变量V执行的后一个动作是store的时候,线程T对变量V才能执行assign动作。所以,线程T对变量V的assign操作和线程T对变量V的store、write动作相关联,必须一起连续出现。也即是在线程T的工作内存中,每次修改变量V之后必须立刻同步回主内存,用于保证线程T对变量V的修改能立刻被其他线程看到。

  • 假定动作A是线程T对变量V实施的use或assign动作,动作F是和动作A相关联的load或store动作,动作P是和动作F相对应的对变量V的read或write动作;类似的,假定动作B是线程T对变量W实施的use或assign动作,动作G是和动作B相关联的load或store动作,动作Q是和动作G相对应的对变量W的read或write动作。如果动作A先于B,那么P先于Q。也就是说在同一个线程内部,被volatile修饰的变量不会被指令重排序,保证代码的执行顺序和程序的顺序相同。

总结上面三条规则,前面两条可以概括为:volatile类型的变量保证对所有线程的可见性。第三条为:volatile类型的变量禁止指令重排序优化。下面分别说明一下:

  • valatile类型的变量保证对所有线程的可见性
    可见性是指当一个线程修改了这个变量的值,新值(修改后的值)对于其他线程来说是立即可以得知的。正如上面的前两条规则规定,volatile类型的变量每次值被修改了就立即同步回主内存,每次使用时就需要从主内存重新读取值。返回到前面对普通变量的规则中,并没有要求这一点,所以普通变量的值是不会立即对所有线程可见的。
    误解:volatile变量对所有线程是立即可见的,所以对volatile变量的所有修改(写操作)都立刻能反应到其他线程中。或者换句话说:volatile变量在各个线程中是一致的,所以基于volatile变量的运算在并发下是线程安全的。
    这个观点的论据是正确的,但是根据论据得出的结论是错误的,并不能得出这样的结论。volatile的规则,保证了read、load、use的顺序和连续行,同理assign、store、write也是顺序和连续的。也就是这几个动作是原子性的,但是对变量的修改,或者对变量的运算,却不能保证是原子性的。如果对变量的修改是分为多个步骤的,那么多个线程同时从主内存拿到的值是最新的,但是经过多步运算后回写到主内存的值是有可能存在覆盖情况发生的。如下代码的例子:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    public class VolatileTest {
    private static final int THREADS_NUM = 20;
    public static volatile int count = 0;

    public static void increase() {
    count++;
    }

    public static void main(String[] args) throws InterruptedException {
    CountDownLatch latch = new CountDownLatch(THREADS_NUM);
    Thread[] threads = new Thread[THREADS_NUM];

    for (int i = 0; i < THREADS_NUM; i++) {
    threads[i] = new Thread(() -> {
    for (int j = 0; j < 10000; j++) {
    increase();
    }

    latch.countDown();
    });

    threads[i].start();
    }

    latch.await();

    System.out.println(count);
    }
    }

代码就是对volatile类型的变量启动了20个线程,每个线程对变量执行1w次加1操作,如果volatile变量并发操作没有问题的话,那么结果应该是输出20w,但是结果运行的时候每次都是小于20w,这就是因为count++操作不是原子性的,是分多个步骤完成的。假设两个线程a、b同时取到了主内存的值,是0,这是没有问题的,在进行++操作的时候假设线程a执行到一半,线程b执行完了,这时线程b立即同步给了主内存,主内存的值为1,而线程a此时也执行完了,同步给了主内存,此时的值仍然是1,线程b的结果被覆盖掉了。

  • volatile变量禁止指令重排序优化
    普通的变量仅仅会保证在该方法执行的过程中,所有依赖赋值结果的地方都能获取到正确的结果,但不能保证变量赋值的操作顺序和程序代码的顺序一致。因为在一个线程的方法执行过程中无法感知到这一点,这也就是java内存模型中描述的所谓的“线程内部表现为串行的语义”。也就是在单线程内部,我们看到的或者感知到的结果和代码顺序是一致的,即使代码的执行顺序和代码顺序不一致,但是在需要赋值的时候结果也是正确的,所以看起来就是串行的。但实际结果有可能代码的执行顺序和代码顺序是不一致的。这在多线程中就会出现问题。
    看下面的伪代码举例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    Map<String, String> configOptions;
    char[] configText;
    // volatile类型变量
    volatile boolean initialized = false;

    // 假设以下代码在线程A中执行
    // 模拟读取配置信息,读取完成后认为是初始化完成
    configOptions = new HashMap();
    configText = readConfigFile(fileName);
    processConfigOptions(configText, configOptions);
    initialized = true;

    // 假设以下代码在线程B中执行
    // 等待initialized为true后,读取配置信息进行操作
    while (!initialized) {
    sleep();
    }
    doSomethingWithConfig();

如果initialiezd是普通变量,没有被volatile修饰,那么线程A执行的代码的修改初始化完成的结果initialized = true就有可能先于之前的三行代码执行,而此时线程B发现initialized为true了,就执行doSomethingWithConfig()方法,但是里面的配置信息都是null的,就会出现问题了。现在initialized是volatile类型变量,保证禁止代码重排序优化,那么就可以保证initialized = true执行的时候,前边的三行代码一定执行完成了,那么线程B读取的配置文件信息就是正确的。

由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁(使用synchronized或java.util.concurrent中的原子类)来保证原子性。

  1. 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值;
  2. 变量不需要与其他的状态变量共同参与不变约束。

像下面的代码就很适合使用volatile变量来控制并发,当shutdown方法被调用时,能保证所有线程中执行的doWork()方法都立即停下来。

1
2
3
4
5
6
7
8
9
volatile boolean shutdownRequest;

public void shutdown() {
shutdownRequest = true;
}

while(!shutdownRequest) {
// do stuff
}

线程状态转换

Java语言定义了5种线程状态,在任意一个时间点,一个线程只能有且只有其中的一种状态,这5种状态分别如下:

  • 新建(New):创建后尚未启动的线程处于这种状态;
  • 运行(Runnable):Runnable包括了操作系统线程状态中的Running和Ready,也就是处于此状态的线程有可能正在执行,也有可能正在等待着CPU为它分配执行时间;
  • 无限期等待(Waiting):处于这种状态的线程不会被分配CPU执行时间,它们要等待被其他线程显式地唤醒。以下方法会让线程陷入无限期的等待状态:
    • 没有设置Timeout参数的Object.wait()方法;
    • 没有设置Timeout参数的Thread.join()方法;
    • LockSupport.park()方法;
  • 限期等待(Timed Waiting):处于这种状态的线程也不会被分配CPU执行时间,不过无须等待被其他线程显式地唤醒,在一定的时间之后它们会由系统自动唤醒。以下方法会让线程进入限期等待状态:
    • Thread.sleep()方法;
    • 设置了Timeout参数的Object.wait()方法;
    • 设置了Timeout参数的Thread.join()方法;
    • LockSupport.parkNanos()方法;
    • LockSupport.parkUntil()方法;
  • 阻塞(Blocked):线程被阻塞了,“阻塞状态”与“等待状态”的区别是:“阻塞状态”在等待着获取到一个排他锁,这个事件将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态。
  • 结束(Terminated):已终止线程的线程状态,线程已经执行结束。

上述5种状态在遇到特定事件发生的时候会互相转换,它们的转换关系如下图所示:

Java生成Heap Dump及OOM问题排查

引用自 https://www.baeldung.com/java-heap-dump-capture
A heap dump is a snapshot of all the objects that are in memory in the JVM at a certain moment. They are very useful to troubleshoot memory-leak problems and optimize memory usage in Java applications.

Heap dumps are usually stored in binary format hprof files. We can open and analyze these files using tools like jhat or JVisualVM. Also, for Eclipse users it’s very common to use MAT.

1. jmap
jmap is a tool to print statistics about the memory in a running JVM. We can use it for local or remote processes.

To capture a heap dump using jmap we need to use the dump option:

jmap -dump:[live],format=b,file=<file-path> <pid>

Along with that option, we should specify several parameters:

  • live: if set it only prints objects which have active references and discards the ones that are ready to be garbage collected. This parameter is optional
  • format=b: specifies that the dump file will be in binary format. If not set the result is the same
  • file: the file where the dump will be written to
  • pid: id of the Java process

An example would be like this:

jmap -dump:live,format=b,file=/tmp/dump.hprof 12587

Remember that we can easily get the pid of a Java process by using the jps command.
Keep in mind that jmap was introduced in the JDK as an experimental tool and it’s unsupported. Therefore, in some cases, it may be preferable to use other tools instead.

2. jcmd
jcmd is a very complete tool which works by sending command requests to the JVM. We have to use it in the same machine where the Java process is running.

One of its many commands is the GC.heap_dump. We can use it to get a heap dump just by specifying the pid of the process and the output file path:

jcmd <pid> GC.heap_dump <file-path>

We can execute it with the same parameters that we used before:

jcmd 12587 GC.heap_dump /tmp/dump.hprof

As with jmap, the dump generated is in binary format.

3. JVisualVM
JVisualVM is a tool with a graphical user interface that lets us monitor, troubleshoot and profile Java applications. The GUI is simple but very intuitive and easy to use.

One of its many options allows us to capture a heap dump. If we right-click on a Java process and select the “Heap Dump” option, the tool will create a heap dump and open it in a new tab:

jvisualvm

Notice that we can find the path of the file created in the “Basic Info” section.

Capture a Heap Dump Automatically
All the tools that we’ve shown in the previous sections are intended to capture heap dumps manually at a specific time. In some cases, we want to get a heap dump when a java.lang.OutOfMemoryError occurs so it helps us investigate the error.

For these cases, Java provides the HeapDumpOnOutOfMemoryError command-line option that generates a heap dump when a java.lang.OutOfMemoryError is thrown:

java -XX:+HeapDumpOnOutOfMemoryError

By default, it stores the dump in a java_pid<pid>.hprof file in the directory where we’re running the application. If we want to specify another file or directory we can set it in the HeapDumpPath option:

java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=<file-or-dir-path>

When our application runs out of memory using this option, we’ll be able to see in the logs the created file that contains the heap dump:

java.lang.OutOfMemoryError: Requested array size exceeds VM limit
Dumping heap to java_pid12587.hprof ...
Exception in thread "main" Heap dump file created [4744371 bytes in 0.029 secs]
java.lang.OutOfMemoryError: Requested array size exceeds VM limit
    at com.baeldung.heapdump.App.main(App.java:7)

In the example above, it was written to the java_pid12587.hprof file.

As we can see, this option is very useful and there is no overhead when running an application with this option. Therefore, it’s highly recommended to use this option always, especially in production.

Finally, this option can also be specified at runtime by using the HotSpotDiagnostic MBean. To do so, we can use JConsole and set the HeapDumpOnOutOfMemoryError VM option to true:

4. JMX
The last approach that we’ll cover in this article is using JMX. We’ll use the HotSpotDiagnostic MBean that we briefly introduced in the previous section. This MBean provides a dumpHeap method that accepts 2 parameters:

  • outputFile: the path of the file for the dump. The file should have the hprof extension
  • live: if set to true it dumps only the active objects in memory, as we’ve seen with jmap before

In the next sections, we’ll show 2 different ways to invoke this method in order to capture a heap dump.

4.1. JConsole
The easiest way to use the HotSpotDiagnostic MBean is by using a JMX client such as JConsole.

If we open JConsole and connect to a running Java process, we can navigate to the MBeans tab and find the HotSpotDiagnostic under com.sun.management. In operations, we can find the dumpHeap method that we’ve described before:

As shown, we just need to introduce the parameters outputFile and live into the p0 and p1 text fields in order to perform the dumpHeap operation.

4.2. Programmatic Way
The other way to use the HotSpotDiagnostic MBean is by invoking it programmatically from Java code.

To do so, we first need to get an MBeanServer instance in order to get an MBean that is registered in the application. After that, we simply need to get an instance of a HotSpotDiagnosticMXBean and call its dumpHeap method.

Let’s see it in code:

public static void dumpHeap(String filePath, boolean live) throws IOException {
    MBeanServer server = ManagementFactory.getPlatformMBeanServer();
    HotSpotDiagnosticMXBean mxBean = ManagementFactory.newPlatformMXBeanProxy(
    server, "com.sun.management:type=HotSpotDiagnostic", HotSpotDiagnosticMXBean.class);
    mxBean.dumpHeap(filePath, live);
}

Notice that an hprof file cannot be overwritten. Therefore, we should take this into account when creating an application that prints heap dumps. If we fail to do so we’ll get an exception:

Exception in thread "main" java.io.IOException: File exists
at sun.management.HotSpotDiagnostic.dumpHeap0(Native Method)
at sun.management.HotSpotDiagnostic.dumpHeap(HotSpotDiagnostic.java:60)

5. Conclusion
In this tutorial, we’ve shown multiple ways to capture a heap dump in Java.

As a rule of thumb, we should remember to use the HeapDumpOnOutOfMemoryError option always when running Java applications. For other purposes, any of the other tools can be perfectly used as long as we keep in mind the unsupported status of jmap.

we can open the file in Java VisualVM by choosing File -> Load from the main menu.

使用jstat命令查看jvm的GC情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ jps
130014 Jps
129998 Test

$ jstat -gc 129998 2000
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
5120.0 5120.0 0.0 0.0 31744.0 3810.3 84992.0 0.0 4480.0 774.3 384.0 75.9 0 0.000 0 0.000 0.000
5120.0 5120.0 0.0 0.0 31744.0 3810.3 84992.0 0.0 4480.0 774.3 384.0 75.9 0 0.000 0 0.000 0.000
5120.0 5120.0 0.0 0.0 31744.0 3810.3 84992.0 0.0 4480.0 774.3 384.0 75.9 0 0.000 0 0.000 0.000
5120.0 5120.0 0.0 0.0 31744.0 3810.3 84992.0 0.0 4480.0 774.3 384.0 75.9 0 0.000 0 0.000 0.000
5120.0 5120.0 0.0 0.0 31744.0 3810.3 84992.0 0.0 4480.0 774.3 384.0 75.9 0 0.000 0 0.000 0.000
5120.0 5120.0 0.0 0.0 31744.0 3810.3 84992.0 0.0 4480.0 774.3 384.0 75.9 0 0.000 0 0.000 0.000
5120.0 5120.0 0.0 0.0 31744.0 3810.3 84992.0 0.0 4480.0 774.3 384.0 75.9 0 0.000 0 0.000 0.000
5120.0 5120.0 0.0 0.0 31744.0 3810.3 84992.0 0.0 4480.0 774.3 384.0 75.9 0 0.000 0 0.000 0.000
5120.0 5120.0 0.0 0.0 31744.0 3810.3 84992.0 0.0 4480.0 774.3 384.0 75.9 0 0.000 0 0.000 0.000
5120.0 5120.0 0.0 0.0 31744.0 3810.3 84992.0 0.0 4480.0 774.3 384.0 75.9 0 0.000 0 0.000 0.000

使用下面2个命令也行:

jstat -gcutil 129998 2000
jstat -gccause 129998 2000

上面的命令,最后2个参数指:进程号,时间间隔。示例的是,每2秒显示一次进程号为129998的java进程的GC情况。一些参数说明如下:
Column Description
S0C Current survivor space 0 capacity (KB).
S1C Current survivor space 1 capacity (KB).
S0U Survivor space 0 utilization (KB).
S1U Survivor space 1 utilization (KB).
EC Current eden space capacity (KB).
EU Eden space utilization (KB).
OC Current old space capacity (KB).
OU Old space utilization (KB).
PC Current permanent space capacity (KB).
PU Permanent space utilization (KB).
YGC Number of young generation GC Events.
YGCT Young generation garbage collection time.
FGC Number of full GC events.
FGCT Full garbage collection time.
GCT Total garbage collection time.

更多选项,请参见:
https://docs.oracle.com/javase/7/docs/technotes/tools/share/jstat.html

声明:图片来源于网络,仅用于学习使用。

mongo 学习笔记

安装mongodb数据库

首先,我们必须安装mongodb,这样才能使用。我们下载相应版本的mongodb,因为我的笔记本电脑是ubuntu,所以我下载mongodb的tgz版本,下载地址如下:

https://www.mongodb.com/download-center/community

下载之后,得到如下两个文件:
mongodb-linux-x86_64-ubuntu1804-4.0.6.tgz
mongodb-linux-x86_64-ubuntu1804-4.0.6.tgz.md5

mongodb依赖libcurl4、openssl,我们必须先安装这两个依赖包:

1
2
3
4
5
6
如已安装,则可跳过,验证命令如下
$ dpkg -s libcurl4
$ dpkg -s openssl

安装命令
$ sudo apt-get install libcurl4 openssl

我们将压缩包下载到/home/hewentian/ProjectD目录,然后解压:

1
2
3
4
5
$ cd /home/hewentian/ProjectD
$ tar xf mongodb-linux-x86_64-ubuntu1804-4.0.6.tgz
$
$ ls mongodb-linux-x86_64-ubuntu1804-4.0.6/
bin LICENSE-Community.txt MPL-2 README THIRD-PARTY-NOTICES

创建data目录和log目录

1
2
3
$ cd /home/hewentian/ProjectD
$ mkdir -p db/mongodb/data
$ mkdir -p db/mongodb/log

创建mongod.conf配置文件

1
2
$ cd /home/hewentian/ProjectD/mongodb-linux-x86_64-ubuntu1804-4.0.6/
$ vi mongod.conf

并在其中输入如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# mongod.conf

# for documentation of all options, see:
# http://docs.mongodb.org/manual/reference/configuration-options/

# Where and how to store data.
storage:
dbPath: /home/hewentian/ProjectD/db/mongodb/data
journal:
enabled: true
# engine:
# mmapv1:
# wiredTiger:

# where to write logging data.
systemLog:
destination: file
logAppend: true
path: /home/hewentian/ProjectD/db/mongodb/log/mongod.log

# network interfaces
net:
port: 27017
bindIp: 127.0.0.1,192.168.56.110 # 127.0.0.1只能从本机访问,将本机的IP配置到这里,这样其他机器才能通过该IP访问
# bindIp: 0.0.0.0


# how the process runs
processManagement:
fork: true # fork and run in background
pidFilePath: /home/hewentian/ProjectD/db/mongodb/mongod.pid # location of pidfile
timeZoneInfo: /usr/share/zoneinfo

#security:
# authorization: enabled

#operationProfiling:

#replication:

#sharding:

## Enterprise-Only Options:

#auditLog:

#snmp:

启动mongodb

1
2
3
4
5
6
$ cd /home/hewentian/ProjectD/mongodb-linux-x86_64-ubuntu1804-4.0.6/bin
$ ./mongod -f ../mongod.conf

about to fork child process, waiting until server is ready for connections.
forked process: 24049
child process started successfully, parent exiting

停止mongodb

不要直接通过kill命令杀掉mongodb的进程,而应通过它官方提供的关闭脚本,如下:

1
2
3
4
$ cd /home/hewentian/ProjectD/mongodb-linux-x86_64-ubuntu1804-4.0.6/bin
$ ./mongod -f ../mongod.conf --shutdown

killing process with pid: 24049

配置登录用户名和密码

要开启密码登录,首先要将mongod.conf中的以下选项打开:

security:
  authorization: enabled

然后重启mongodb服务。

  1. 添加管理员
    使用mongo命令进入命令行交互模式,创建第一个用户admin,该用户需要有用户管理权限,其角色为root。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    $ cd /home/hewentian/ProjectD/mongodb-linux-x86_64-ubuntu1804-4.0.6/bin
    $ ./bin/mongo --host 127.0.0.1
    MongoDB shell version v4.0.6
    connecting to: mongodb://127.0.0.1:27017/?gssapiServiceName=mongodb
    Implicit session: session { "id" : UUID("cbd0445f-667b-4d2a-92a8-66f8ad1d07ef") }
    MongoDB server version: 4.0.6
    > use admin
    switched to db admin
    > db.createUser({user:"admin",pwd:"admin",roles:["root"]})
    Successfully added user: { "user" : "admin", "roles" : [ "root" ] }
    >
    > show collections
    Warning: unable to run listCollections, attempting to approximate collection names by parsing connectionStatus
    > db.auth("admin","admin")
    1
    > show collections
  2. 添加数据库用户
    为数据库添加用户,添加用户前需要切换到该数据库,这里简单设置其角色为dbOwner

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    > use bfg
    switched to db bfg
    > db.createUser({user: "bfg", pwd: "bfg100", roles: [{ role: "dbOwner", db: "bfg" }]})
    Successfully added user: {
    "user" : "bfg",
    "roles" : [
    {
    "role" : "dbOwner",
    "db" : "bfg"
    }
    ]
    }

这样,bfg的用户就只能访问bfg这个库了,登录方式如下:

1
2
3
4
5
6
7
8
9
10
11
$ ./bin/mongo --host 127.0.0.1
MongoDB shell version v4.0.6
connecting to: mongodb://127.0.0.1:27017/?gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("873114cf-fb7e-436a-b939-7e765dbecee9") }
MongoDB server version: 4.0.6
> use bfg
switched to db bfg
> db.auth("bfg","bfg100")
1
> show dbs
bfg 0.000GB

至此,数据库安装完毕(此安装过程2019年初才补上)。

修改用户密码

修改用户密码使用db.changeUserPassword(username, password),Updates a user’s password. Run the method in the database where the user is defined, i.e. the database you created the user.

shell命令行方式连接到mongodb

连接到单机:

1
$ mongo --host 192.168.1.111:27017 --authenticationDatabase user_database -u user_name -p user_password

连接到单机中的指定数据库:

1
$ mongo "192.168.1.111:27017/user_database" --authenticationDatabase user_database -u user_name -p user_password

连接到副本集:

1
$ mongo "192.168.1.111:27017,192.168.1.112:27017/user_database?replicaSet=mgset-9527&readPreference=secondaryPreferred" --authenticationDatabase user_database -u user_name -p user_password

mongodb查询数组大小

mongodb查询数组大小使用$size,例如:我们有一个名为person的集合,其中有个字段为childrenNames,是数组类型,如果我们要查询childrenNames长度为2的数据,则查询语句为:

db.getCollection('person').find({'childrenNames':{'$size':2}})

但是它不能限定数组的大小范围,只能查询指定的长度。要查询数组范围,我们使用$exists,例如:查询数组长度大于等于3的语句如下(检查数组第3个元素是否存了)

db.getCollection('person').find({'childrenNames.2':{'$exists':1}})

数组查询

有如下数组,它的查询方式为:
db.getCollection(‘userInfo’).find({“sons”: {$elemMatch: {“birthday”: {$gte:1594252800000, $lte:1594339200000}}}})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"_id" : ObjectId("5f0e26bcf0a06369a460b4c8"),
"name" : "tony",
"sons" : [
{
"name" : "h",
"birthday" : NumberLong(1594762936000),
},
{
"name" : "w",
"birthday" : NumberLong(1594762936000),
}
]
}

mongodb分组查询

如果我们要对集合person中的地区字段area来分组统计,语法如下:

db.getCollection('person').aggregate([{'$group':{'_id':'$area','count':{'$sum':1}}}])

如果我们还想将分组统计结果,按数量倒序输出显示:

db.getCollection('person').aggregate([{'$group':{'_id':'$area','count':{'$sum':1}}},{'$sort':{'count':-1}}])

对应的JAVA代码如下:

1
2
3
4
5
6
7
8
9
MongoDatabase mongoDatabase = MongoUtil.getMongoDatabase(); // 这个是我自已写的工具类
MongoCollection<Document> personCollection = mongoDatabase.getCollection("person");

List<BasicDBObject> pipeline = new ArrayList<BasicDBObject>();
pipeline.add(new BasicDBObject("$group", new BasicDBObject("_id", "$area").append("count", new BasicDBObject("$sum", 1))));
pipeline.add(new BasicDBObject("$sort", new BasicDBObject("count", -1))); // 如果不要排序,则注释这一行

AggregateIterable<Document> aggregate = personCollection.aggregate(pipeline);
MongoCursor<Document> iterator = aggregate.iterator();

mongodb更新部分字段

例如我们要将person_id123的数据的address修改为:广东,语句如下:

db.getCollection('person').update({'_id':'123'},{$set:{'address':'广东'}})

删除数据库

1
2
3
> show dbs
> use monitor
> db.dropDatabase()

删除集合

1
2
> show collections
> db.collection_name.drop()

删除数据

删除数据的时候,要注意条件为null的情况,这在组成json条件的时候,会变成{},这会将整个库的数据都删掉,这个要避免。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
MongoCollection<Document> mongoCollection = null; // 这里不建连接,仅演示
JSONArray userNames = new JSONArray();
for (String userName : Arrays.asList("", null, "xiao ming")) {
JSONObject obj = new JSONObject();
obj.put("userName", userName);
userNames.add(obj);
}

JSONObject condition = new JSONObject();
condition.put("$or", userNames);

// 删除原有的数据
log.info("删除的语句为:" + condition); // {"$or":[{"userName":""},{},{"userName":"xiao ming"}]}
DeleteResult deleteResult = mongoCollection.deleteMany(BsonDocument.parse(condition.toJSONString()));

log.info("======mongo删除原有的数据条数:======" + deleteResult.getDeletedCount());

导出、导入数据库

我们使用mongoexport来导出指定的collection

该命令位于{MONGO_HOME}/bin/目录下,可以把一个collection导出成JSON或CSV格式的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
语法:
$ mongoexport -h {SERVER_ADDRESS} --port {SERVER_PORT} -u {USER_NAME} -p {PASSWORD} --authenticationDatabase {AUTH_DB} -d {DB_NAME} -c {COLLECTION_NAME} -o {EXPORT_FILE_PATH} --type json/csv -f fields

参数说明:

-h: MONGODB服务器的地址
--port: MONGODB服务器的端口
-u: 用户名
-p: 密码
--authenticationDatabase: 验证数据库
-d: 数据库名
-c: collection名
-o: 输出的文件名
--type: 输出的格式,默认为json
-f: 输出的字段,如果-type为csv,则需要加上-f "字段名"

示例:
$ mongoexport -h 127.0.0.1 --port 27017 -u bfg_user -p a12345678 --authenticationDatabase admin -d bfg -c user -o /home/hewentian/ProjectD/db/user.json
$ mongoexport -h 127.0.0.1 --port 27017 -u bfg_user -p a12345678 --authenticationDatabase admin -d bfg -c user -o /home/hewentian/ProjectD/db/user.json --type json -f "_id,name"
$ mongoexport -h 127.0.0.1 --port 27017 -u bfg_user -p a12345678 --authenticationDatabase admin -d bfg -c user -o /home/hewentian/ProjectD/db/user.csv --type csv -f "_id,name"

如果导出的数据中包含查询条件,则要用下面这种方式导出:

1
$ mongo "192.168.1.111:27017/user_database" --authenticationDatabase user_database -u user_name -p user_password --quiet --eval 'db.user.find({ _id: {$gt: ObjectId("5ee08d67e144cb56edf945da")}}).forEach(printjson);' > a.json

如果导出的查询条件中包含ObjectId,则要用$oid来代替它:

1
mongoexport -h 127.0.0.1 --port 27017 -u bfg_user -p a12345678 --authenticationDatabase admin -d bfg -c user -o /home/hewentian/ProjectD/db/user.json -q '{"_id":{"$oid":"5ece9b4d8008d750e611010c"}}'

我们使用mongoimport来导入指定的collection

该命令位于{MONGO_HOME}/bin/目录下,可以把一个json/csv文件导入到指定的collection

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
语法:
$ mongoimport -h {SERVER_ADDRESS} --port {SERVER_PORT} -u {USER_NAME} -p {PASSWORD} --authenticationDatabase {AUTH_DB} -d {DB_NAME} -c {COLLECTION_NAME} --file {IMPORT_FILE_PATH} --headerline --type json/csv -f fields

参数说明:

-h: MONGODB服务器的地址
--port: MONGODB服务器的端口
-u: 用户名
-p: 密码
--authenticationDatabase: 验证数据库
-d: 数据库名
-c: collection名
--file: 要导入的文件
--type: input format to import: json, csv, or tsv (defaults to 'json') (default: json)
--headerline: use first line in input source as the field list (CSV and TSV only)
-f: 导入的字段名,CSV或TSV的时候可用

示例:
$ mongoimport -h 127.0.0.1 --port 27017 -u bfg_user -p a12345678 --authenticationDatabase admin -d bfg -c user --file /home/hewentian/ProjectD/db/user.json

注意:--headerline 和 -f 不能同时使用

--headerline: 使用CSV文件中首列指定的列名作为导入的name
-f: 会将CSV文件的所有列,作为数据进行导入,包括第一列的列名(如果文件中有指定列名)

$ mongoimport -h 127.0.0.1 --port 27017 -u bfg_user -p a12345678 --authenticationDatabase admin -d bfg -c user --file /home/hewentian/ProjectD/db/user.csv --type csv --headerline
$ mongoimport -h 127.0.0.1 --port 27017 -u bfg_user -p a12345678 --authenticationDatabase admin -d bfg -c user --file /home/hewentian/ProjectD/db/user.csv --type csv -f "_id,name,age"

我们使用mongodump来导出指定的database

该命令位于{MONGO_HOME}/bin/目录下,可以把一个database导出成指定目录下的JSON、BSON的文件集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
语法:
$ mongodump -h {SERVER_ADDRESS} --port {SERVER_PORT} -u {USER_NAME} -p {PASSWORD} --authenticationDatabase {AUTH_DB} -d {DB_NAME} -o {DUMP_FILE_PATH}

参数说明:

-h: MONGODB服务器的地址
--port: MONGODB服务器的端口
-u: 用户名
-p: 密码
--authenticationDatabase: 验证数据库
-d: 数据库名
-o: 输出的文件路径,要事先创建该目录,在该目录下会自动创建要导出的数据库作为子目录

示例:
$ mongodump -h 127.0.0.1 --port 27017 -u bfg_user -p a12345678 --authenticationDatabase admin -d bfg -o /home/hewentian/ProjectD/db/

我们使用mongorestore来恢复指定的database

该命令位于{MONGO_HOME}/bin/目录下,可以把指定目录下的JSON、BSON的文件集恢复成database

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
语法:
$ mongorestore -h {SERVER_ADDRESS} --port {SERVER_PORT} -u {USER_NAME} -p {PASSWORD} --authenticationDatabase {AUTH_DB} -d {DB_NAME} --dir {RESTORE_FILE_PATH}

参数说明:

-h: MONGODB服务器的地址
--port: MONGODB服务器的端口
-u: 用户名
-p: 密码
--authenticationDatabase: 验证数据库
-d: 数据库名
--dir: 要恢复的数据库的位置,注意与上面备份的 -o 不同,这里要指定到数据库的目录

示例:
$ mongorestore -h 127.0.0.1 --port 27017 -u bfg_user -p a12345678 --authenticationDatabase admin -d bfg --dir /home/hewentian/ProjectD/db/bfg/

mongodb类型转换

如下所示,将字符串类型的数据转换为int, double, date类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
先插入一条数据,都是字符串类型:
db.getCollection("userInfo").insert({"age":"20", "salary":"30", "birthday":"2018-12-21"})

{
"_id" : ObjectId("5c45b7099a14ab9807edaa75"),
"age" : "20",
"salary" : "30",
"birthday" : "2018-12-21"
}

改变字段类型:
db.getCollection("userInfo").find({}).forEach(function(doc) {
db.getCollection('userInfo').updateOne({_id: doc._id}, {$set: {"age": NumberInt(doc.age), "salary": parseInt(doc.salary), "birthday": new ISODate(doc.birthday)}})
})

{
"_id" : ObjectId("5c45b7099a14ab9807edaa75"),
"age" : 20,
"salary" : 30.0,
"birthday" : ISODate("2018-12-21T00:00:00.000Z")
}

如果要将集合中已有文档的ISODate类型转换成String类型的日期,例如:将updateTimeISODate("2019-04-04T07:12:37.295Z")转换为2019-04-04 07:12:37类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
db.getCollection('userInfo').find({"updateTime":{"$type":9}}).forEach(function(doc) {
var mydate = doc.updateTime;
if (mydate) {
mydate = mydate.toJSON();
if (mydate.length > 19) {
mydate = mydate.substr(0, 19)
mydate = mydate.replace('T',' ');
}
}

db.getCollection('userInfo').update({"_id":doc._id}, {"$set":{"updateTime":mydate}})
})


验证结果:
db.getCollection('userInfo').find({"_id":ObjectId("5abc7ffc00a32c2b045e598c")})

MongoDB的主键类型修改

主键类型的修改不能像其他字段一样直接修改

将String类型的主键修改为ObjectId类型,在Mongodb中String类型对应的int值为2,
我们先增加一条主键为ObjectId记录,然后删除主键为String的记录。

1
2
3
4
5
6
db.getCollection('userInfo').find({"_id": {"$type": 2}}).forEach(function(doc) {
doc._id = ObjectId(doc._id);
db.getCollection('userInfo').save(doc);
})

db.getCollection('userInfo').remove({"_id": {"$type": 2}})

反之,将ObjectId类型的主键修改为String类型,在Mongodb中ObjectId类型对应的int值为7。

1
2
3
4
5
6
db.getCollection('userInfo').find({"_id": {"$type": 7}}).forEach(function(doc) {
doc._id = doc._id.toJSON().$oid;
db.getCollection('userInfo').save(doc);
})

db.getCollection('userInfo').remove({"_id": {"$type": 7}})

mongodb默认插入类型

插入的int32整数会默认转为Double类型,若需插入为整数,需指定NumberInt:

1
2
3
4
5
6
7
8
9
db.getCollection("userInfo").insert({"age":20, "salary":NumberInt(30), "birthday":"2018-12-21"})

db.getCollection("userInfo").find({})
{
"_id" : ObjectId("5d3815a2a7de52e9fed7bbcf"),
"age" : 20.0,
"salary" : 30,
"birthday" : "2018-12-21"
}

mongodb模糊查询

1
db.getCollection('userInfo').find({"name":{"$regex":"t"}})

这样名字中包含”tom”和”scott”的记录都会查询出来。

mongodb日期查询

日期的类型不同,查询的方式也不同:

1
2
3
4
5
6
如果日期的类型为String:
db.getCollection('userInfo').find({"insertTime":{$regex:"2019-01-07 *"}}) // *号前有个空格
db.getCollection('userInfo').find({"insertTime":{$gte:"2019-02-01 00:00:00",$lte:"2019-02-11 23:59:59"}})

如果日期的类型为ISODate:
db.getCollection('userInfo').find({"insertTime":{$gte:ISODate("2019-01-01T00:00:00Z"),$lte:ISODate("2019-02-11T23:59:59Z")}})

复制集合

在复制集合的时候,修改某些值,可以使用javascript实现(建议在mongo shell下执行,这样不容易超时),示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var count = 0;
var totalCount = db.getCollection('user').count({});
var lastId = "000000000000000000000000";

while(count < totalCount) {
db.getCollection('user').find({"_id":{"$gt":lastId}}).sort({"_id":1}).limit(100).forEach((doc)=> {
if (++count % 1000 == 0) {
print('handling: ' + count + ' / ' + totalCount)
}

lastId = doc._id;
doc._id=doc.regId;
db.getCollection('user_new').save(doc);
});
}

正则表达式的使用

查询用户名name前后有空格的记录:

1
2
db.getCollection('userInfo').find({"name":/^\s+|\s+$/})
db.getCollection('userInfo').find({"$or":[{"name":/^\s+|\s+$/}, {"postCode":/^\s+|\s+$/}]})

删除字段

The following update() operation uses the $unset operator to remove the fields quantity and instock from the first document in the products collection where the field sku has a value of unknown.

1
2
3
4
db.products.update(
{ sku: "unknown" },
{ $unset: { quantity: "", instock: "" } }
)

if you want to update all matched, use updateMany instead.

mongo执行一个js脚本文件

例如有一个js脚本文件,位于/home/hewentian/Documents/updateMongo.js,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/*
* 处理字段中数据重复的方法:将字符串一分为二,如果前后两部分相同,则更新成其中一部分
*/

var handleCount = 0;
var updateCount = 0;
var totalCount = db.getCollection('userInfo').count({});
var lastId = ObjectId('000000000000000000000000');

while (handleCount < totalCount) {
db.getCollection('userInfo').find({"_id": {"$gt": lastId}}).sort({"_id": 1}).limit(5).forEach((doc) =>{
if (++handleCount % 10 == 0 || handleCount == totalCount) {
print('handling: ' + handleCount + ' / ' + totalCount + ", updateCount = " + updateCount + ", lastId = " + lastId.toJSON().$oid + (handleCount == totalCount ? " end" : ""));
}

lastId = doc._id;

var userName = doc.userName;

if (userName) { // 这个字段必须要存在
var len = userName.length;

if (len % 2 == 0) { // 字符数必须为偶数倍
var middleIndex = len / 2;
var substrA = userName.substr(0, middleIndex);
var substrB = userName.substr(middleIndex);

if (substrA == substrB) { // 前后两半字符串相同时,才更新
db.getCollection('userInfo').update({"_id": doc._id}, {"$set": {"userName": substrA}})
updateCount++;
}
}
}
});
}

我们这样执行这个脚本:

1
$ mongo "192.168.1.111:27017/user_database" --authenticationDatabase user_database -u user_name -p user_password /home/hewentian/Documents/updateMongo.js

使用typeof操作符

有时候,我们在使用javascript操作Mongodb的过程中,可能要根据数据类型进行相关操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*
* 对集合进行清洗,保证字段 telephone 是字符串格式
*/

var handleCount = 0;
var updateCount = 0;
var totalCount = db.getCollection('userInfo').count({});
var lastId = "000000000000000000000000";

while (handleCount < totalCount) {
db.getCollection('userInfo').find({"_id": {"$gt": lastId}}).sort({"_id": 1}).limit(100).forEach((doc) =>{
if (++handleCount % 1000 == 0 || handleCount == totalCount) {
print('handling: ' + handleCount + ' / ' + totalCount + ", updateCount = " + updateCount + ", lastId = " + lastId + (handleCount == totalCount ? " end" : ""));
}

lastId = doc._id;

var telephone = doc.telephone;

if (telephone && typeof(telephone) == 'object') { // 这个字段必须要存在,并且是内嵌类型文档
var fixTelephone = telephone.phoneNumber;
if (fixTelephone && typeof(fixTelephone) == 'string') {
db.getCollection('userInfo').updateOne({"_id": doc._id}, {"$set": {"telephone": fixTelephone}});
updateCount++;
}
}
});
}

MongoDB 去重(distinct)查询后求总数(count)

  1. 直接使用distinct语句查询,这种查询会将所有查询出来的数据返回给用户,然后对查询出来的结果集求总数(耗内存,耗时一些)
    db.student.distinct("name", {"age" : 18}).length
    

使用这种方法查询时,查询的结果集大于16M时会查询失败:
{“message” : “distinct failed: MongoError: distinct too big, 16mb cap”,”stack” : “script:1:20”}

  1. 使用聚合函数,多次分组统计结果,最终将聚合的结果数返回给用户
    db.student.aggregate([
        {$match:{"age" : 18}},
        {$project:{"name":1}},
        {$group:{"_id":"$name","count":{$sum:1}}},
        {$sort:{"count":-1}}
    ])
    

这种查询数据量大时就不会出现如上查询失败的情况,而且这种查询不管是内存消耗还是时间消耗都优于上面一种查询。

BSON ObjectID Specification

A BSON ObjectID is a 12-byte value consisting of a 4-byte timestamp (seconds since epoch), a 3-byte machine id,
a 2-byte process id, and a 3-byte counter. Note that the timestamp and counter fields must be stored big endian
unlike the rest of BSON. This is because they are compared byte-by-byte and we want to ensure a mostly increasing
order. The format:

0 1 2 3    4 5 6    7 8    9 10 11
time       machine    pid       inc
  • TimeStamp: This is a unix style timestamp. It is a signed int representing the number of seconds before or after January 1st 1970 (UTC).
  • Machine: This is the first three bytes of the (md5) hash of the machine host name, or of the mac/network address, or the virtual machine id.
  • Pid: This is 2 bytes of the process id (or thread id) of the process generating the object id.
  • Increment: This is an ever incrementing value, or a random number if a counter can’t be used in the language/runtime.

BSON ObjectIds can be any 12 byte binary string that is unique; however, the server itself and almost all drivers use the format above.

ObjectId占用12字节的存储空间,由“时间戳” 、“机器名”、“PID号”和“计数器”组成。使用机器名的好处是在分布式环境中能够避免
单点计数的性能瓶颈。使用PID号的好处是支持同一机器内运行多个mongod实例。最终采用时间戳和计数器的组合来保证唯一性。

自动生成的主键objectId是一个24位的字符串,它是由一组十六进制的字符构成,每个字节两位的十六进制数字,总共用了12字节的存储空间。

  • 时间戳
    确保ObjectId唯一性依赖的是时间的顺序,不依赖时间的取值,因此集群节点的时间不必完全同步。既然ObjectId已经有了时间戳,
    那么在文档中就可以省掉一个时间戳了。在使用ObjectID提取时间时,应注意到MongoDB允许各节点时间不一致这一细节。

  • 机器名
    机器名通过Md5加密后取前三个字节,应该还是有重复概率的,配置生产集群时检查一下总不会错。另外,我也注意到重启MongoDB后
    MD5加密结果会发生变化,在利用ObjectID提取机器名信息时需格外注意。

  • PID号
    注意到每次重启mongod进程后PID号通常会发生变化就可以了。

  • 计数器
    计数器占3个字节,表示的取值范围就是256*256*256-1=16777215。不妨认为MongDB性能的极限是单台设备一秒钟插入一千万条记录。
    以目前的水平看,单台设备一秒钟插入一万条就很不错了,因此ObjectID计数器的设计是够用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> db.user.findOne()._id
ObjectId("5eda936e8008d750e63ee723")

> db.user.findOne()._id.toString()
ObjectId("5eda936e8008d750e63ee723")

> db.user.findOne()._id.toJSON()
{ "$oid" : "5eda936e8008d750e63ee723" }

> db.user.findOne()._id.toJSON().$oid.substring(0, 8)
5eda936e

> db.user.findOne()._id.getTimestamp()
ISODate("2020-06-06T02:48:14Z")
  • 从mongoDB的ObjectId中提取时间信息
    取8个字符,得到的是这条数据创建时的时间戳(不带毫秒位数),在后面补上毫秒位数”000”。

java代码

1
2
3
4
5
6
7
8
// ObjectId("5eda936e8008d750e63ee723")
String id = "5eda936e8008d750e63ee723";

// 取前8位
long timestamp = Long.parseLong(Integer.parseInt(id.substring(0, 8), 16) + "000");

Date date = new Date(timestamp);
System.out.println(date); // Sat Jun 06 02:48:14 CST 2020

javascript代码

1
2
3
4
5
6
7
8
9
// ObjectId("5eda936e8008d750e63ee723")
var id = "5eda936e8008d750e63ee723";

// 取前8位
var timestamp = Number(parseInt(id.substring(0, 8), 16).toString() + "000");

var date = new Date(timestamp);

console.log(date); // Sat Jun 06 2020 02:48:14 GMT+0800 (China Standard Time)

explain查询执行情况

explain的目的是将mongo的黑盒操作白盒化。比如查询很慢的时候想知道原因。explain有三种模式:

  1. queryPlanner: 不会真正的执行查询,只是分析查询,选出winningPlan。
  2. executionStats: 返回winningPlan的关键数据。
  3. allPlansExecution: 执行所有的plans。

通过explain(“executionStats”)来选择模式,默认是第一种模式。一些返回字段的说明:
namespace: 本次所查询的集合
indexFilterSet: 是否使用partial index,比如只对某个集合中的部分文档进行index
parsedQuery: 本次执行的查询
executionTimeMillis: 该query查询的总体时间
indexName: 所使用的索引的名字
indexBounds: 索引查找时使用的范围
stage:
COLLSCAN: 全表扫描
IXSCAN: 索引扫描
FETCH: 根据索引去检索指定document
SHARD_MERGE: 将各个分片返回数据进行merge
SORT: 表明在内存中进行了排序
LIMIT: 使用limit限制返回数
SKIP: 使用skip进行跳过
IDHACK: 针对_id进行查询

通过这些信息就能判断查询时如何执行的了。示例如下,先插入3个文档:

1
2
3
db.getCollection("userInfo").insert({"age":"20", "name":"scott", "birthday":"2018-12-21"})
db.getCollection("userInfo").insert({"age":"21", "name":"tiger", "birthday":"2019-12-21"})
db.getCollection("userInfo").insert({"age":"23", "name":"tom", "birthday":"1919-11-21"})

没有建立索引的时候查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
db.getCollection("userInfo").find({"name":"tom"}).explain()
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "bfg.userInfo",
"indexFilterSet" : false,
"parsedQuery" : {
"name" : {
"$eq" : "tom"
}
},
"queryHash" : "01AEE5EC",
"planCacheKey" : "01AEE5EC",
"winningPlan" : {
"stage" : "COLLSCAN",
"filter" : {
"name" : {
"$eq" : "tom"
}
},
"direction" : "forward"
},
"rejectedPlans" : [ ]
},
"serverInfo" : {
"host" : "0d550468977f",
"port" : 27017,
"version" : "4.4.0",
"gitVersion" : "563487e100c4215e2dce98d0af2a6a5a2d67c5cf"
},
"ok" : 1
}

"stage" : "COLLSCAN"可知,没有使用到索引,是全表扫描的。我们建个索引,再查询。

db.getCollection('userInfo').ensureIndex({"name":1}, {background:true})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
db.getCollection("userInfo").find({"name":"tom"}).explain()
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "bfg.userInfo",
"indexFilterSet" : false,
"parsedQuery" : {
"name" : {
"$eq" : "tom"
}
},
"queryHash" : "01AEE5EC",
"planCacheKey" : "4C5AEA2C",
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"name" : 1
},
"indexName" : "name_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"name" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"name" : [
"[\"tom\", \"tom\"]"
]
}
}
},
"rejectedPlans" : [ ]
},
"serverInfo" : {
"host" : "0d550468977f",
"port" : 27017,
"version" : "4.4.0",
"gitVersion" : "563487e100c4215e2dce98d0af2a6a5a2d67c5cf"
},
"ok" : 1
}

这次可以看到,它使用了索引。

监控

mongodb可以通过profile来监控数据,进行优化。查看当前是否开启profile功能用命令:`db.getProfilingLevel()`

返回level等级,值为0|1|2,分别代表意思:0代表关闭,1代表记录慢命令,2代表全部。开启profile功能为
db.setProfilingLevel(level),level为1的时候,慢命令默认值为100ms,更改为db.setProfilingLevel(level, slowms)
db.setProfilingLevel(1,50)这样就更改为50毫秒。通过db.system.profile.find()查看当前的监控日志。通过执行
db.system.profile.find({millis:{$gt:500}})能够返回查询时间在500毫秒以上的查询命令。

这里值的含义
op: query,代表查询
ns: 代表查询的库与集合
command: 命令的内容
responseLength: 返回的结果集大小,byte数
nscanned: 扫描记录数量
filter: 后面是查询条件
nreturned: 返回记录数
ts: 命令执行的时刻
millis: 所花时间

如果发现时间比较长,那么就需要作优化。比如nscanned数很大,或者接近记录总数,那么可能没有用到索引查询。
responseLength很大,有可能返回没必要的字段。
nreturned很大,那么有可能查询的时候没有加限制。

mongo可以通过db.serverStatus()查看mongod的运行状态
mongo可以通过db.currentOp()可以查看当前正在执行的操作。这两个命令,必须用admin帐号才能操作。

如果出现如下错误提示,则是由于auth太多了,退出,并且以admin帐号登录admin库来执行
mongo –host 192.168.1.100 –port 27017 –authenticationDatabase admin -u admin -p admin

1
2
3
4
5
6
7
db.currentOp()
{
"ok" : 0,
"errmsg" : "too many users are authenticated",
"code" : 13,
"codeName" : "Unauthorized"
}

solr 集群的创建

solr 集群,即 solrcloud 的创建
下面说说solr集群的安装使用,同样是使用前面例子使用的 solr6.5.0 版本,我在一台机器上安装,所以这是伪集群,当修改为真集群的时候,只要将IP地址修改下即可,下面会说明。

首先,你得搭建 zookeeper 集群,可以参考 zookeeper集群版安装方法

下面开始创建solr集群:

创建一个目录用于存放集群使用到的所有实例和配置信息

1
2
3
$ cd /home/hewentian/ProjectD
$ mkdir solrCloud # 存放集群的所有文件,包括:三个solr实例、和实例共享的配置文件solr-configs
$ mkdir solrCloud/solr-configs # 用于存放集群的core的配置信息,会被上传到zookeeper

将一个SOLR压缩包放到这个目录,我之前已在Downloads目录下载好了

1
2
3
4
5
6
$ cp /home/hewentian/Downloads/solr-6.5.0.tgz ./
$ tar xzvf solr-6.5.0.tgz
$ mv solr-6.5.0 solr1 # 为方便起见,这里将其重命名为solr1,先将solr1配置好,后面会将其复制为solr2, solr3

$ ls
solr1 solr-6.5.0.tgz solr-configs

我们仍然使用之前的例子,演示使用DIH方式将数据导入到SOLR,只不过,这次是集群的方式,所以需要将data_driven_schema_configs/conf的配置信息复制到solr-configs中,并将其命名为mysqlCore

1
2
3
4
$ pwd
/home/hewentian/ProjectD/solrCloud

$ cp -r solr1/server/solr/configsets/data_driven_schema_configs/conf/ solr-configs/mysqlCore/

将mysql-connector-java-5.1.25.jar放到solr1/server/lib/ext目录下面,并将上一个例子,solr的安装使用的那三个文件: solrconfig.xml、db-data-config.xml、managed-schema复制到solr-configs/mysqlCore/下,override存在的。

接下来还有2个文件需要修改:
修改文件一:solr.in.sh

1
2
3
4
5
6
$ vi solr1/bin/solr.in.sh

# 修改以下内容即可
ZK_HOST="127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183"
ZK_CLIENT_TIMEOUT="15000"
SOLR_PORT=8983

修改文件二:solr.xml

1
2
3
4
5
$ vi solr1/server/solr/solr.xml

# 修改下面的内容即可
<str name="host">${host:127.0.0.1}</str> # 当修改为真集群的时候,只修改这个IP地址即可
<int name="hostPort">${jetty.port:8983}</int> # 这里端口为 8983

这样一个实例就配置好了,将其复制为solr2, solr3

1
2
$ cp -r solr1 solr2
$ cp -r solr1 solr3

修改solr2, solr3的端口为8984, 8985

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ vi solr2/bin/solr.in.sh

# 修改以下内容即可
SOLR_PORT=8984

$ vi solr3/bin/solr.in.sh

# 修改以下内容即可
SOLR_PORT=8985

$ vi solr2/server/solr/solr.xml

# 修改下面的内容即可
<str name="host">${host:127.0.0.1}</str> # 当修改为真集群的时候,只修改这个IP地址即可
<int name="hostPort">${jetty.port:8984}</int> # 这里端口为 8984

$ vi solr3/server/solr/solr.xml

# 修改下面的内容即可
<str name="host">${host:127.0.0.1}</str> # 当修改为真集群的时候,只修改这个IP地址即可
<int name="hostPort">${jetty.port:8985}</int> # 这里端口为 8985

这样,三个solr实例就已经配置好了,下面将其都启动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ pwd
/home/hewentian/ProjectD/solrCloud

$ solr1/bin/solr start
Waiting up to 180 seconds to see Solr running on port 8983 [\]
Started Solr server on port 8983 (pid=15292). Happy searching!

$ solr2/bin/solr start
Waiting up to 180 seconds to see Solr running on port 8984 [\]
Started Solr server on port 8984 (pid=15507). Happy searching!

$ solr3/bin/solr start
Waiting up to 180 seconds to see Solr running on port 8985 [\]
Started Solr server on port 8985 (pid=15706). Happy searching!

在浏览器中可以如下访问:
http://localhost:8983
http://localhost:8984
http://localhost:8985

下面,我们将solr-configs/mysqlCore的内容上传到zookeeper,以便让三台solr都使用这个配置

1
$ /home/hewentian/ProjectD/solrCloud/solr1/server/scripts/cloud-scripts/zkcli.sh -zkhost 127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183 -cmd upconfig -confdir /home/hewentian/ProjectD/solrCloud/solr-configs/mysqlCore -confname mysqlCore

说明:

  1. -confdir:这个指的是 本地上传的文件位置
  2. -confname:上传后在zookeeper中的节点名称

也可以上传单个文件,单个文件上传先要删除,不然会报错:

1
$ /home/hewentian/ProjectD/solrCloud/solr1/server/scripts/cloud-scripts/zkcli.sh -zkhost 127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183 -cmd putfile /configs/mysqlCore/solrconfig.xml /home/hewentian/ProjectD/solrCloud/solr-configs/mysqlCore/solrconfig.xml

说明:

putfile 后第一个/configs/mysqlCore/solrconfig.xml指的是zookeeper中的配置文件,第二个/home/hewentian/ProjectD/solrCloud/solr-configs/mysqlCore/solrconfig.xml指的是本地文件路径

下面我们在solr1中创建一个core,并命名为 mysqlCore,在浏览器中输入如下url即可,它有3个分片,每个分片有2个副本:
http://localhost:8983/solr/admin/collections?action=CREATE&name=mysqlCore&numShards=3&replicationFactor=2&maxShardsPerNode=3&collection.configName=mysqlCore
执行成功后,你可能会看到如下输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
This XML file does not appear to have any style information associated with it. The document tree is shown below.
<response>
<lst name="responseHeader">
<int name="status">0</int>
<int name="QTime">16020</int>
</lst>
<lst name="success">
<lst name="127.0.0.1:8983_solr">
<lst name="responseHeader">
<int name="status">0</int>
<int name="QTime">13930</int>
</lst>
<str name="core">mysqlCore_shard3_replica2</str>
</lst>
<lst name="127.0.0.1:8983_solr">
<lst name="responseHeader">
<int name="status">0</int>
<int name="QTime">14273</int>
</lst>
<str name="core">mysqlCore_shard2_replica1</str>
</lst>
<lst name="127.0.0.1:8984_solr">
<lst name="responseHeader">
<int name="status">0</int>
<int name="QTime">14479</int>
</lst>
<str name="core">mysqlCore_shard3_replica1</str>
</lst>
<lst name="127.0.0.1:8985_solr">
<lst name="responseHeader">
<int name="status">0</int>
<int name="QTime">14597</int>
</lst>
<str name="core">mysqlCore_shard2_replica2</str>
</lst>
<lst name="127.0.0.1:8985_solr">
<lst name="responseHeader">
<int name="status">0</int>
<int name="QTime">14748</int>
</lst>
<str name="core">mysqlCore_shard1_replica1</str>
</lst>
<lst name="127.0.0.1:8984_solr">
<lst name="responseHeader">
<int name="status">0</int>
<int name="QTime">14778</int>
</lst>
<str name="core">mysqlCore_shard1_replica2</str>
</lst>
</lst>
</response>

也可以使用下面的方式,指定在哪台机器中创建哪个分片的哪个副本。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ curl "http://localhost:8983/solr/admin/cores?action=CREATE&name=mysqlCore_shard1_replica1&instanceDir=mysqlCore_shard1_replica1&collection=mysqlCore&shard=mysqlCore_shard1"

$ curl "http://localhost:8983/solr/admin/cores?action=CREATE&name=mysqlCore_shard3_replica2&instanceDir=mysqlCore_shard3_replica2&collection=mysqlCore&shard=mysqlCore_shard3"


$ curl "http://localhost:8984/solr/admin/cores?action=CREATE&name=mysqlCore_shard1_replica2&instanceDir=mysqlCore_shard1_replica2&collection=mysqlCore&shard=mysqlCore_shard1"

$ curl "http://localhost:8984/solr/admin/cores?action=CREATE&name=mysqlCore_shard2_replica1&instanceDir=mysqlCore_shard2_replica1&collection=mysqlCore&shard=mysqlCore_shard2"


$ curl "http://localhost:8985/solr/admin/cores?action=CREATE&name=mysqlCore_shard2_replica2&instanceDir=mysqlCore_shard2_replica2&collection=mysqlCore&shard=mysqlCore_shard2"

$ curl "http://localhost:8985/solr/admin/cores?action=CREATE&name=mysqlCore_shard3_replica1&instanceDir=mysqlCore_shard3_replica1&collection=mysqlCore&shard=mysqlCore_shard3"

你也可以查看分片是否创建成功:

1
2
3
4
5
6
7
8
$ ls solr1/server/solr
configsets mysqlCore_shard2_replica1 mysqlCore_shard3_replica2 README.txt solr.xml zoo.cfg

$ ls solr2/server/solr
configsets mysqlCore_shard1_replica2 mysqlCore_shard3_replica1 README.txt solr.xml zoo.cfg

$ ls solr3/server/solr
configsets mysqlCore_shard1_replica1 mysqlCore_shard2_replica2 README.txt solr.xml zoo.cfg

http://localhost:8983上面执行DIH将user索引进solr,成功之后你就可以在http://localhost:8984, http://localhost:8985上面看到同样的结果。

当然,我们也可以在启动的时候,才指定zookeeper

1
$ ./bin/solr start -c -z zk1:port,zk2:port,zk3:port

说明:

  1. -c:以solr_cloud的方式启动;
  2. -z:指定zookeeper集群的地址和端口,上面搭建zookeeper集群时的3台机器

solr备份

在进行solr备份的时候,一定要先将solr服务停了,然后再备份。否则在还原的时候有可能会产生write.lock,在产生write.lock的时候也不要怕,你可以按下面的方法解决:

  1. write.lock删掉,然后再新建一个,然后修改权限;

    1
    2
    3
    $ rm write.lock
    $ touch write.lock
    $ chmod 666 write.lock
  2. 重启solr服务;

  3. 在浏览器登录到那个solr的cord,reload一下,然后查询,如果没报错的话,可能要等5分钟左右,查询结果就出来了。
    [solr界面]->[collections]->[点击你的那个 collection]->[Reload]

JAVA示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
CloudSolrClient cloudSolrClient = new CloudSolrClient.Builder()
.withZkHost("127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183").build();
cloudSolrClient.setDefaultCollection("mysqlCore");

SolrQuery solrQuery = new SolrQuery();

solrQuery.setQuery("schools:\"北京第一中学\" AND id:[1 TO 3]");

// 分页
solrQuery.setStart(0);
solrQuery.setRows(10);

SolrDocumentList results = cloudSolrClient.query(solrQuery).getResults();

logger.info("numFound: " + results.getNumFound());
if (CollectionUtils.isEmpty(results)) {
return;
}

results.stream().forEach(logger::info);
}

未完,待续……

neo4j 学习笔记

一些基本概念,从官网copy过来的,如下:

Graph Fundamentals

Basic concepts to get you going.

A graph database can store any kind of data using a few simple concepts:

  1. Nodes - graph data records
  2. Relationships - connect nodes
  3. Properties - named data values

A Graph Database

Neo4j stores data in a Graph, with records called Nodes.

The simplest graph has just a single node with some named values called Properties. Let’s draw a social graph of our friends on the Neo4j team:

  1. Start by drawing a circle for the node
  2. Add the name Emil
  3. Note that he is from Sweden
  • Nodes are the name for data records in a graph
  • Data is stored as Properties
  • Properties are simple name/value pairs

Labels

Associate a set of nodes.

Nodes can be grouped together by applying a Label to each member. In our social graph, we’ll label each node that represents a Person.

  1. Apply the label “Person” to the node we created for Emil
  2. Color “Person” nodes red
  • A node can have zero or more labels
  • Labels do not have any properties

More Nodes

Schema-free, nodes can have a mix of common and unique properties.

Like any database, storing data in Neo4j can be as simple as adding more records. We’ll add a few more nodes:

  1. Emil has a klout score of 99
  2. Johan, from Sweden, who is learning to surf
  3. Ian, from England, who is an author
  4. Rik, from Belgium, has a cat named Orval
  5. Allison, from California, who surfs
  • Similar nodes can have different properties
  • Properties can be strings, numbers, or booleans
  • Neo4j can store billions of nodes

Consider Relationships

Connect nodes in the graph

The real power of Neo4j is in connected data. To associate any two nodes, add a Relationship which describes how the records are related.

In our social graph, we simply say who KNOWS whom:

  1. Emil KNOWS Johan and Ian
  2. Johan KNOWS Ian and Rik
  3. Rik and Ian KNOWS Allison
  • Relationships always have direction
  • Relationships always have a type
  • Relationships form patterns of data

Relationship properties

Store information shared by two nodes.

In a property graph, relationships are data records that can also contain properties. Looking more closely at Emil’s relationships, note that:

  • Emil has known Johan since 2001
  • Emil rates Ian 5 (out of 5)
  • Everyone else can have similar relationship properties

可以使用这种方式访问neo4j数据库:

1
http://localhost:7474/browser/

清空neo4j数据库

1
2
3
match(n)
optional match(n)-[r]-()
delete n,r

执行上面的命令后,会删除节点及与该节点相关的关系,但是property keys删不干净

未完,待续……

gitbook 学习笔记

下面说下如何安装gitbook

1. 安装 Node.js

先测试一下Node.js是否已安装,在命令行中直接输入node可以看到提示符变成了一个向右
的箭头就表示成功了,然后按ctrl + c退出node模式,出现$符号才表示正常了

如果未安裝 node,安裝方法如下:

1
$ sudo apt-get install nodejs-legacy

剩下的安装过程参考https://yq.aliyun.com/articles/7506,但是会有一些不同,如下所示:

2. 安装gitbook

1
$ sudo npm install -g gitbook-cli

3. 初始化

在你的文档目录下新建文件 SUMMARY.md,这个文件就是这本书的目录啦:

1
2
$ cd docs
$ touch SUMMARY.md

SUMMARY.md 的格式规范如下:

# uitest 文档

- [uitest 是什么](users/index.md)
    - [如何使用 uitest](users/use.md)
    - [如何编写自定义的测试用例](users/case.md)
    - [browserjs API 文档](users/api.md)
- [uitest 开发者文档](devs/index.md)
    - [browserjs 开发者文档](devs/browserjs.md)
    - [utci 文档](devs/utci.md)
    - [utserver & utclient 文档](devs/utserver.md)
- [相关文章沉淀](artical.md)
- [关于 gitbook](gitbook.md)

然后执行gitbook init初始化,gitbook 会根据 SUMMARY 的结构生成对应的目录文件:

├── README.md           // 首页
├── SUMMARY.md          // 目录
└── users               // 用户文档
    └── index.md        // 是什么
    ├── use.md          // 如何使用
    ├── api.md          // browserjs API
    ├── case.md         // 如何写测试用例
├── devs                // 开发者文档目录
    │   ├── index.md        // 开发者文文档首页
    │   ├── browserjs.md    // browserjs 开发文档
    │   ├── utci.md         // utci 开发文档
    │   └── utserver.md     // utserver 和 utclien 开发文档
├── artical.md          // 文章沉淀
├── gitbook.md          // gitbook 相关

4. 本地调试

在对应的文档目录下运行gitbook serve会启动一个本地的静态服务器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ cd docs
$ gitbook serve

Live reload server started on port: 35729
Press CTRL+C to quit ...

info: 7 plugins are installed
info: loading plugin "livereload"... OK
info: loading plugin "highlight"... OK
info: loading plugin "search"... OK
info: loading plugin "lunr"... OK
info: loading plugin "sharing"... OK
info: loading plugin "fontsettings"... OK
info: loading plugin "theme-default"... OK
info: found 27 pages
info: found 2 asset files

访问 http://localhost:4000/ 就可以实时的预览啦,并且支持livereload, 灰常赞~接下来结合预览的功能编辑对应的文档,完成之后就可以发布啦。

5. 发布

在文档目录下执行gitbook build会生成一个_book的目录,这个目录就是我们的静态网站啦,然后通过 demo 平台或者 github pages 就可以很简单的完成部署了。

activeMQ 学习笔记

Installation Procedure for Unix

Unix Binary Installation
This procedure explains how to download and install the binary distribution on a Unix system.
NOTE: There are several alternative ways to perform this type of installation.

  1. Download the activemq zipped tarball file to the Unix machine, using either a browser or a tool, i.e., wget, scp, ftp, etc. for example:
    (see Download -> “The latest stable release”)

    1
    $ wget http://activemq.apache.org/path/tofile/apache-activemq-x.x.x-bin.tar.gz
  2. Extract the files from the zipped tarball into a directory of your choice. For example:

    1
    2
    $ cd [activemq_install_dir]
    $ tar zxvf activemq-x.x.x-bin.tar.gz

Starting ActiveMQ

On Unix:
From a command shell, change to the installation directory and run ActiveMQ as a foregroud process:

1
2
$ cd [activemq_install_dir]/bin
$ ./activemq console

From a command shell, change to the installation directory and run ActiveMQ as a daemon process:

1
2
$ cd [activemq_install_dir]/bin
$ ./activemq start

Stopping ActiveMQ

For both Windows and Unix installations, terminate ActiveMQ by typing “CTRL-C” in the console or command shell in which it is running.
If ActiveMQ was started in the background on Unix, the process can be killed, with the following:

1
2
$ cd [activemq_install_dir]/bin
$ ./activemq stop

Testing the Installation

Using the administrative interface

  • Open the administrative interface
  • URL: http://127.0.0.1:8161/admin/
  • Login: admin
  • Passwort: admin
  • Navigate to “Queues”
  • Add a queue name and click create
  • Send test message by klicking on “Send to”

Listen port

ActiveMQ’s default port is 61616. From another window run netstat and search for port 61616.From a Unix command shell, type:

1
2
$ netstat -nl | grep 61616
tcp6 0 0 :::61616 :::* LISTEN

部分示例可以在这里找到。

安全问题

ObjectMessage objects depend on Java serialization of marshal/unmarshal object payload. This process is generally considered unsafe as malicious payload can exploit the host system. That’s why starting with versions 5.12.2 and 5.13.0, ActiveMQ enforces users to explicitly whitelist packages that can be exchanged using ObjectMessages.

ActiveMQ从5.12.2、5.13.0开始,如果传送的消息是一个JavaBean就要设置一个传输对象包名的白名单列表,否则会报如下错:

javax.jms.JMSException: Failed to build body from content. Serializable class not available to broker. Reason: java.lang.ClassNotFoundException: Forbidden class java.lang.Integer! This class is not trusted to be serialized as ObjectMessage payload. Please take a look at http://activemq.apache.org/objectmessage.html for more information on how to configure trusted classes.
at org.apache.activemq.util.JMSExceptionSupport.create(JMSExceptionSupport.java:36)
at org.apache.activemq.command.ActiveMQObjectMessage.getObject(ActiveMQObjectMessage.java:213)
at com.hewentian.activemq.bean.Consumer$1.onMessage(Consumer.java:48)
at org.apache.activemq.ActiveMQMessageConsumer.dispatch(ActiveMQMessageConsumer.java:1404)
at org.apache.activemq.ActiveMQSessionExecutor.dispatch(ActiveMQSessionExecutor.java:131)
at org.apache.activemq.ActiveMQSessionExecutor.iterate(ActiveMQSessionExecutor.java:202)
at org.apache.activemq.thread.PooledTaskRunner.runTask(PooledTaskRunner.java:133)
at org.apache.activemq.thread.PooledTaskRunner$1.run(PooledTaskRunner.java:48)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)

根据提示,我们打开http://activemq.apache.org/objectmessage.html,可以发现解决此报错的详细说明。解决方法有二:
注意: 我是用方法二解决的,方法一试了,无效。

  1. The setTrustedPackages() method allows you to set the list of trusted packages you want to be to unserialize, like

    1
    2
    ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory("tcp://localhost:61616");
    factory.setTrustedPackages(new ArrayList(Arrays.asList("org.apache.activemq.test,com.hewentian.activemq.bean".split(","))));
  2. The setTrustAllPackages() allows you to turn off security check and trust all classes. It’s useful for testing purposes.

    1
    2
    ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory("tcp://localhost:61616");
    factory.setTrustAllPackages(true);

如果是springBoot工程,你也可以在application.properties作如下设置:

1
2
3
4
5
spring.activemq.packages.trust-all=true

or

spring.activemq.packages.trusted=<package1>,<package2>,<package3>

消息的消费者接收消息可以采用两种方式:

  1. consumer.receive() 或 consumer.receive(int timeout);
  2. 使用setMessageListener。
    采用第一种方式,消息的接收者会一直等待下去,直到有消息到达,或者超时。后一种方式会注册一个监听器,当有消息到达的时候,会回调它的onMessage()方法。

rabbitMQ 学习笔记

在安装好rabbitMQ后,执行rabbitmq-plugins enable rabbitmq_management命令,开启Web管理插件,这样我们就可以通过浏览器来进行管理了。默认地址为:
http://localhost:15672/
并使用默认用户guest登录,密码也为guest。这个guest用户只能在安装rabbitMQ的机器上登录,如果要在其他机器登录,则用guest登录后,再创建其他用户即可,创建的用户要授权,否则用API是无法访问的,有可能会报错。因为新创建的用户默认是没有权限访问/的,可以在WEB上面授权,或者用命令授权。
列出用户权限

1
2
3
4
5
6
7
8
$ sudo rabbitmqctl list_users

Listing users ...
hewentian [administrator]
guest [administrator]

授权
$ sudo rabbitmqctl set_permissions -p / hewentian '.*' '.*' '.*'

该命令使用户hewentian具有/这个virtual host中所有资源的配置、写、读权限以便管理其中的资源

其使用也是非常容易入门的:
在spring boot项目的application.properties中配置关于rabbitMQ的连接和用户信息

1
2
3
4
5
6
spring.application.name=rabbitmq-demo

spring.rabbitmq.host=10.1.32.97
spring.rabbitmq.port=5672
spring.rabbitmq.username=hewentian
spring.rabbitmq.password=12345678

创建消息生产者Sender,将消息发送到myqueue这个队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class Sender {

@Autowired
private AmqpTemplate rabbitTemplate;

public void send() {
String context = "hello " + new Date();
this.rabbitTemplate.convertAndSend("myqueue", context);
}
}

创建消息消费者Receiver,对队列myqueue进行监听

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
@RabbitListener(queues = "myqueue")
public class Receiver {

@RabbitHandler
public void process(String ctx) {
System.out.println("Receiver : " + ctx);
}
}

创建RabbitMQ的配置类RabbitConfig

1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitConfig {

@Bean
public Queue helloQueue() {
return new Queue("myqueue");
}
}

这样一个简单的例子就完成了。更多详细例子,请参考这里。另外,下面这两篇文章,值得我们认真看下:
http://www.rabbitmq.com/amqp-0-9-1-quickref.html
http://www.rabbitmq.com/tutorials/amqp-concepts.html

The default exchange is a direct exchange with no name (empty string) pre-declared by the broker.

Direct Exchange

Direct exchanges are often used to distribute tasks between multiple workers (instances of the same application) in a round robin manner. When doing so, it is important to understand that, in AMQP 0-9-1, messages are load balanced between consumers and not between queues.

Fanout Exchange

A fanout exchange routes messages to all of the queues that are bound to it and the routing key is ignored. If N queues are bound to a fanout exchange, when a new message is published to that exchange a copy of the message is delivered to all N queues. Fanout exchanges are ideal for the broadcast routing of messages.

Because a fanout exchange delivers a copy of a message to every queue bound to it, its use cases are quite similar:

  • Massively multi-player online (MMO) games can use it for leaderboard updates or other global events
  • Sport news sites can use fanout exchanges for distributing score updates to mobile clients in near real-time
  • Distributed systems can broadcast various state and configuration updates
  • Group chats can distribute messages between participants using a fanout exchange (although AMQP does not have a built-in concept of presence, so XMPP may be a better choice)

Topic Exchange

Topic exchanges route messages to one or many queues based on matching between a message routing key and the pattern that was used to bind a queue to an exchange. The topic exchange type is often used to implement various publish/subscribe pattern variations. Topic exchanges are commonly used for the multicast routing of messages.

Topic exchanges have a very broad set of use cases. Whenever a problem involves multiple consumers/applications that selectively choose which type of messages they want to receive, the use of topic exchanges should be considered.

Example uses:

  • Distributing data relevant to specific geographic location, for example, points of sale
  • Background task processing done by multiple workers, each capable of handling specific set of tasks
  • Stocks price updates (and updates on other kinds of financial data)
  • News updates that involve categorization or tagging (for example, only for a particular sport or team)
  • Orchestration of services of different kinds in the cloud
  • Distributed architecture/OS-specific software builds or packaging where each builder can handle only one architecture or OS

Headers Exchange

A headers exchange is designed for routing on multiple attributes that are more easily expressed as message headers than a routing key. Headers exchanges ignore the routing key attribute. Instead, the attributes used for routing are taken from the headers attribute. A message is considered matching if the value of the header equals the value specified upon binding.

It is possible to bind a queue to a headers exchange using more than one header for matching. In this case, the broker needs one more piece of information from the application developer, namely, should it consider messages with any of the headers matching, or all of them? This is what the “x-match” binding argument is for. When the “x-match” argument is set to “any”, just one matching header value is sufficient. Alternatively, setting “x-match” to “all” mandates that all the values must match.

Headers exchanges can be looked upon as “direct exchanges on steroids”. Because they route based on header values, they can be used as direct exchanges where the routing key does not have to be a string; it could be an integer or a hash (dictionary) for example.

Consumers

Storing messages in queues is useless unless applications can consume them. In the AMQP 0-9-1 Model, there are two ways for applications to do this:

  • Have messages delivered to them (“push API”)
  • Fetch messages as needed (“pull API”)

With the “push API”, applications have to indicate interest in consuming messages from a particular queue. When they do so, we say that they register a consumer or, simply put, subscribe to a queue. It is possible to have more than one consumer per queue or to register an exclusive consumer (excludes all other consumers from the queue while it is consuming).

Connections

AMQP connections are typically long-lived. AMQP is an application level protocol that uses TCP for reliable delivery. AMQP connections use authentication and can be protected using TLS (SSL). When an application no longer needs to be connected to an AMQP broker, it should gracefully close the AMQP connection instead of abruptly closing the underlying TCP connection.

未完待续。。。