来聊聊NoSql

学习一门技术,我们首先得明白以下几点:

  • 它是什么?
  • 它为什么会出现?
  • 它的出现解决了什么问题?
  • 如何使用?

带着这几个问题去学,我们才能将它的衣服一件件的扒光,最后看到它的本质。不然面试的时候面试官稍微问得深入一点就凉凉了。接下来聊聊NoSql。


欢迎大家关注我的公众号 javawebkf,目前正在慢慢地将简书文章搬到公众号,以后简书和公众号文章将同步更新,且简书上的付费文章在公众号上将免费。


一、什么是NoSql?

NoSql意思是Not Only Sql,不仅仅是SQL。NoSql也被称作非关系型数据库,那么与之相对的就是关系型数据库。你去百度“什么叫关系型数据库”,搜到的是非常官方的答案,说是依据关系模型来创建的数据库。看了半天,所有字都认识,但是连在一起就不知道是什么意思了。这里不搞那些花里胡哨的,用最简单的话说明白:

  • 关系型数据库:以数据表来存储数据,一个pojo对应一张表,表中的一行就是pojo的一个对象,一列就是对象的一个属性,表与表之间的关联代表对象之间的一对一、一对多和多对多的关系。通过Sql对数据表中的数据进行操作。

  • 非关系型数据库:上面说明白了关系型数据库,那么非关系型数据库就好理解了。没有那些对应关系,一般是以键值对的形式存储,不需要通过Sql来进行操作。其实NoSql数据库也不是全都用键值对的形式存储数据,主要有以下四类:

    • 键值存储型(Redis)
    • 文档型(MongoDB)
    • 列储存(Hbase)
    • 图关系数据库(Neo4J)

二、NoSql为何会兴起?

在此之间,先来说说互联网架构的演变(数据库层面)。

  • 单机MySql:互联网刚兴起的时候,由于使用的人数不多,一个网站就是我们IDE中的一个工程,然后发布到服务器上,数据库也就一个MySql就够了。

  • 到后来,用的人多了,数据的读取越来越慢,就在数据库的前面挡了一层缓存,dao层访问的是缓存,而不是直接捅到数据库中取。当时缓存用得多的是Memcached。

  • 再后来,数据库的写入操作越来越多了,由于缓存只能解决读取压力,所以它又行不通了。所以就出现了读写分离。dao层访问缓存,缓存连接着主库和从库,写操作在主库进行,主库再同步到从库,读操作在从库进行,这样来达到读写分离的目的。

  • 随着访问量越来越大,读写分离也搞不定了,就出现了分库分表和数据库集群等技术。

  • 到今天,互联网用户人数已经不是一般的多了,有海量的数据,如果要对海量的数据进行挖掘,关系型数据库就不适用了。所以NoSql就闪亮登场了。本文将介绍用得比较多的NoSQL数据库 --- Redis。

三、NoSQL之Redis

关于redis的安装、五种数据类型以及jedis我在之前的文章中有说过,这里来聊聊剩下的内容。
1、发布与订阅:

  • 什么是发布与订阅:
    发布与订阅,就类似于微信公众号。微信公众号发布了一条消息,所有关注了这个公众号的人都能够收到消息。了解过MQ的人也许会问,MQ也有类似的功能,那么它们之间有什么区别呢?形象地说就是,Redis是兼职,MQ是专职的。MQ它是实现了JMS规范的专门处理消息的消息中间件,其功能远比Redis提供的发布与订阅强大且复杂得多,后续我也会专门说一说MQ,欢迎大家关注。

  • 怎么用?
    其实特别简单,新建两个类,一个订阅者一个发布者。发布者调用publish方法,指定发布的channel和消息内容,而发布者只需继承JedisPubSub类,重写一些方法(主要是为了能够打印订阅情况,为了避免每个订阅者都重写这些方法,自己写一个类去重写这些方法,然后订阅者继承自己这个类就行了),然后调用subscribe方法,指定订阅者和channel即可订阅。代码如下:
    工具类:
public class SubUtil extends JedisPubSub {
    @Override
    public void onMessage(String channel, String message) {
        System.out.println(String.format("receive redis published message, channel %s, message %s", channel, message));
    }

    @Override
    public void onSubscribe(String channel, int subscribedChannels) {
        System.out.println(String.format("subscribe redis channel success, channel %s, subscribedChannels %d",
                channel, subscribedChannels));
    }

    @Override
    public void onUnsubscribe(String channel, int subscribedChannels) {
        System.out.println(String.format("unsubscribe redis channel, channel %s, subscribedChannels %d",
                channel, subscribedChannels));
    }
}

订阅者:

/**
 * 订阅者(先启动订阅者,再启动发布者)
 */
public class SubOne extends SubUtil {
    public static void main(String[] args) throws Exception{
        Jedis jedis = new Jedis("192.168.x.xx", 6379);
        SubOne subOne = new SubOne();
        jedis.subscribe(subOne, "test");
        System.in.read();
    }
}

发布者:

public class Pub {
    public static void main(String[] args) throws Exception {
        Jedis jedis = new Jedis("192.168.2.43",6379);
        long result = jedis.publish("test","hello");
    }
}

这样就搞定了发布与订阅,注意,先启动订阅者,再启动发布者。按顺序启动后会看到如下运行结果:

运行结果

2、管道技术:
在说管道技术之前先说说TCP请求响应模型。

  • 女朋友:你能给我解释一下什么是TCP么?
  • 程序员:我开始解释了:你想听我解释一下TCP 么?
  • 女朋友:嗯,我想听你解释一下TCP 。
  • 程序员:好的,我会给你解释一下什么是TCP 。
  • 女朋友:好的,我会听你解释一下什么是TCP 。
  • 程序员:你准备好听我解释一下什么是TCP了吗 ?
  • 女朋友:嗯,我准备好听你解释一下什么是TCP了。
  • 程序员:Ok,那我要开始解释什么是 TCP 了。大概要 10 秒,20 个字。
  • 女朋友:Ok,我准备收你那个 10 秒时长,20 个字的解释了。

系统提示:抱歉,连接超时了……

  • 女朋友:你能给我解释一下什么是TCP么?
  • 程序员:我开始解释了:你想听我解释一下TCP 么?
    ……

以上就是TCP请求响应模型(本故事纯属虚构,程序员哪来的女朋友😂)。

redis其实也是这种请求响应模型的服务,客户端向服务端发送一个命令,等待服务端的返回;服务端接收到命令进行执行,然后将结果返回给客户端。在这个回合中,服务端是接收不了其他命令的。假如一个回合需要250毫秒,即使服务端一次性能接收100k的请求数,那么1秒钟也只能处理4个请求。使用管道技术就可以解决这种问题。使用管道就相当于可以并发处理,客户端不用等待服务端的响应,继续发起下一个请求。

那么jedis如何使用管道技术呢?请看下面的代码:

public static void main(String[] args) {
        Jedis jedis = new Jedis("192.168.X.XX",6379);
        // 未使用管道技术
        long start1 = System.currentTimeMillis();
        for (int i = 1; i <= 10000; i++) {
            jedis.set(UUID.randomUUID().toString(), String.valueOf(i));
        }
        long end1 = System.currentTimeMillis();
        System.out.println("未使用管道技术插入10000条数据耗时:" + (end1 - start1) + "毫秒");

        // 使用管道技术
        Pipeline pipeline = jedis.pipelined();
        long start2 = System.currentTimeMillis();
        for (int i = 1; i <= 10000 ; i++) {
            pipeline.set(UUID.randomUUID().toString(), String.valueOf(i));
        }
        long end2 = System.currentTimeMillis();
        System.out.println("使用管道技术插入10000条数据耗时:" + (end2 - start2) + "毫秒");
  }

看运行结果:


运行结果

不得不说这管道技术还是有两把刷子的,这耗时相差将近40倍。

3、为key设置过期时间:
我们写入redis中的内容,可以对key设置过期时间。设置key的过期时间,超过时间后,将会自动删除该key(只有执行对key值有影响的操作时才会清除)。设置过期时间的方法是expire,如下:

 jedis.expire("hash",30);

这就是将"hash"这个key设置为30秒后过期。
通过persist方法可以清除过期。

jedis.persist("hash");

这就是将"hash"这个key过期清除掉,也就是相当于上面设置的过期无效了。
如果对一个设置了过期时间的key再次设置过期时间,那么将刷新这个过期时间。比如“hash”这个key我设置了30秒过期,当过了24秒的时候我再次使用expire方法设置它的过期时间为60秒,那么这个key的离过期又还有60秒的时间。

4、redis的事务:
redis的事务,本质是一组命令的集合。所有的命令都会序列化,然后按顺序串行地执行。事务开启后,所有的命令先入队,提交事务的时候,要么全执行,要么全不执行。常用命令如下:

命令 作用
multi 开启事务
exec 提交事务
discard 取消事务
watch key... 监视一个或多个key,如果事务提交前这些key被改动,事务将被打断
unwatch 取消对所有key的监视

下面来使用一下事务:


正常情况

这种是正常执行,如果最后不是exec,而是discard,那就是放弃事务了。接下来看看下面这种情况:


事务中有语法错误

set k3的时候,小手一抖,写成了sete,最后发现事务提交不了,所有的命令都未执行。这种就相当于java中的编译时期就报错了,所以肯定是提交不了的。接下来再看另外一种情况:


运行时异常

开启事务后set了一个String类型的k1,然后让其自增,再set其他的key,最后提交。结果是只有自增的那条命令执行报错,其他的正常执行。

最后来看看事务中的watch和unwatch命令。watch相当于乐观锁,如果watch的key在事务提交前被修改了,那么事务就会提交失败,得重新watch,获取到最新值(watch应该在事务开启之前)。


watch

我开了两个窗口,watch的k1初始值是100,然后在事务中改成200,在事务提交之前,另外一边将其改成了50,现在提交事务就会出现如下结果:


结果

这时候再重新开启事务去修改,如果这个过程没有再被修改,事务才能提交成功,这就类似于CAS。

5、redis执行txt文件中的命令:
如果我们需要执行的redis命令很多,可以写到一个txt文件中,然后读取这个txt文件即可执行里面的命令。具体步骤如下:

  • 创建txt文件:例如我在/srv目录下touch一个redispipedata.txt文件
  • 往文件中写入redis命令:vim打开txt文件,比如我写入如下命令:
set name tom
set age 20
set sex man
set phone 8008208820
set country china
  • 将文件转码:执行如下命令
unix2dos redispipedata.txt

如果找不到命令,yum install 一下即可。

  • 在redis-cli所在的目录下执行如下命令:
cat /srv/redispipedata.txt | ./redis-cli

即可批量执行txt文件中的命令了,执行后可以看到如下结果:


执行结果

上面介绍了管道技术,其实执行txt文件中的命令也可以使用管道技术,前面的步骤都一样,只不过是执行命令改成为:

cat /srv/redispipedata.txt | ./redis-cli --pipe

执行成功的话会看到如下结果:


执行结果

6、redis的持久化:
接触过redis的童鞋一定听过RDB和AOF这两个词,这是redis持久化的两种方式,下面就介绍一下RDB和AOF。

  • RDB(redis database)介绍:
    RDB就是在指定时间间隔内将redis中的数据进行快照存储,默认情况下会将数据写到dump.rdb文件中。保存RDB文件时redis会fork出一个子进程来操作,所以对redis的性能影响很小。但是,由于是隔一段时间保存一次,所以可能会造成一段时间内的数据丢失。假如配置的是每个5分钟保存一次,10点钟的时候保存了一次,10点零4分的时候redis挂掉了,那么10点到10点零4分这段时间的数据就丢失了。

  • AOF(append only file)介绍:
    为什么有了RDB还会出现AOF?就是因为上面说的,10点到10点零4分这段时间的数据会丢失,所以AOF就出现了。AOF会记录redis每次写操作,追加到aof文件中。当redis重启的时候就会重新执行记录的这些操作来恢复数据。由于每次记录写操作都是追加到aof文件中,为了避免AOF文件体积过大,redis还可以对AOF文件进行重写,也就是去除重复的操作,重写完成后,就会立即切换到新的AOF文件,此后的操作都会记录到新的AOF文件中。用户可以自己选择AOF持久化的策略,策略有不同步、每秒钟同步一次和每次进行写操作的时候同步。默认是每秒钟同步一次。所以,采用默认策略的话,最多也就丢失这1秒钟内的写操作。

这两种持久化方式原理其实都是利用了写时复制,redis搞出两个进程,父进程与子进程,RDB方式的时候,子进程以快照形式保存数据,AOF方式的时候,子进程记录写操作。

  • RDB的使用:
    redis默认的持久化方式就是RDB,而且默认开启。RDB保存方式分为主动保存和被动保存。主动保存就是在redis-cli中输入save命令即可,被动保存就是在redis.conf中配置一些触发条件。在redis-cli下使用
config get dir

命令可以查看dump.rdb文件所在的位置。

被动保存:
在redis.conf中,有一项配置叫SNAPSHOTTING,这里就是关于持久化的一些配置。

rdb的配置

这三行配置分别表示:15分钟内改了1次;5分钟内改了10次;1分钟内改了1万次。只要满足这三条中的任意一条,就会将数据保存到dump.rdb中。(测试的时候可以先删除掉dump.rdb文件,然后5分钟内让10个key发生改变,看看是否重新生成了dump.rdb,然后直接让redis shutdown,然后重新启动,keys * 看看之前设置的key是否还在)。

主动保存:
假如你设置的某个key十分重要,你想设置完就写到rdb文件中,那么可以在redis-cli中设置完后用save命令。

主动保存

  • AOF的使用:
    在redis.conf中,有一段名叫APPEND ONLY MODE的配置,这就是AOF的配置。
    AOF配置

可以看到,AOF默认是关闭的,默认文件名为appendonly.aof。上面说到过AOF有三种同步策略,请看下图,确实是3种,我不是吹的。


AOF同步策略

所以,要使用AOF只需要把上面的no改成yes,然后选择策略就可以了。现在的问题是,如果同时存在RDB文件和AOF文件,那么redis启动的时候根据谁来恢复?

测试的办法:vim打开appendonly.aof文件,在末尾加上一些乱七八糟的不正确的指令,保存退出。然后再启动redis,如果redis能够正常启动,说明启动时是优先根据RDB来恢复的,如果不能正常启动,说明优先根据AOF来恢复。接下来就开打:


同时存在RDB和AOF

将appendonly.aof乱改一通:


aof文件

然后启动redis:


启动redis

结果报错了,说明启动时优先根据AOF来恢复数据的。AOF文件中有不可执行的命令,redis启动就会报错,那么怎么修复呢?不要告诉我你打算手动的去将AOF文件中那些不可执行的命令删掉,万一你手一抖多删了怎么办。我们看看redis的src目录:


src目录

没错,就是这两个文件,一个是修复RDB文件的,一个是修复AOF文件的。在src目录下执行

./redis-check-aof --fix appendonly.aof

就可以修复了。

在上面AOF的介绍中提到了会对aof文件进行压缩重写,那么什么时候会触发重写呢?看看配置文件中的相关配置:


重写策略

这两行配置的意思是:当文件大小大于上次rewrite时文件大小的100%且大于64MB时进行重写。其实如果一个公司如果大规模使用redis的话,rewrite-min-size 至少3GB起步。

redis的持久化小总结:
RDB方式性能更好,但是数据完整性不如AOF,所以如果对数据完整性要求不高,使用RDB即可;如果对数据完整性要求高,那么请同时使用RDB和AOF;如果数据量大,使用AOF会进行大量的IO操作,对性能的影响十分明显,那么就使用接下来要介绍的主从复制。

7、redis主从:
所谓主从,就是主从复制,主机数据更新后自动同步到从机的机制,Master以写为主,Slave以读为主。主从可以实现读写分离以及容灾恢复。主从可以分为三种模式,一主二从、薪火相传和反客为主(这里的二是泛指,其实是一主n从)。

(1)、主从之一主二从

  • 首先拷贝三份redis.conf,分别重命名为redismaster.conf、redisslave1.conf和redisslave2.conf。然后要做如下的修改:
    ---- 开启daemonize yes;
    ---- 设置pid文件名字,pidfile /var/run/redisxxxx.pid;
    ---- 指定端口,port xxxx;
    ---- 设置log文件名字,logfile "xxxx.log";
    ---- 设置dump.rdb文件名字,dbfilename dumpxxxx.rdb。
    (xxxx是端口号,三份配置文件端口可以分别为6379、6380、6381)
  • 修改好三份配置文件,接下来就配置一主二仆。
    首先启动这三个redis,然后在redis-cli输入info replication命令,可以看到下图信息:
    info

可以看到,目前启动的三个redis的角色都是master,slave都是0。接下来,在6379的redis中set几个值,然后在6380和6381的redis中输入

slaveof 127.0.0.1 6379

接下来你就会发现,6379的redis中set的值在另外两台机器中也有。


一主二仆

这里我先set 了k1,然后再将6380和6381slaveof 6379的,最后发现get k1也是可以取到值。说明从机会把主机所有数据到拉到从机上,包括监视主机之前主机上的数据。另外,从机是不能set 值的,读写分离,主机负责写,从机是不能写的,在从机set 值会报如下错误:


从机不能写数据

完成了一主二从的配置,看看下面几个问题:

  • 主机挂了,从机上位变主机还是原地待命?


    主机挂了

可以看到,主机挂了,从机还是从机,没有造反。

  • 主机挂了之后复活了,需要重新配置主从体系吗?


    主机复活

可以看到,原先的主从体系并没有土崩瓦解,还可以用。

  • 从机挂了,然后复活。复活之后从机还是从机吗?


    从机挂了

从机挂了再启动,从机就不再是从机了,而变成了另外一个master。所以用slaveof命令配置的从机重启后需要重新配置。

(2)、主从之薪火相传:
上面的一主N从,只有一个boss,所有的小弟都直接跟boss打交道,小弟一多boss也会很累的。所以就有了薪火相传这种模式。6379是主机,6380认6379做大哥,6381认6380做大哥,就这样一脉相承。

薪火相传

将6381这台从机挂在6380下,这就是薪火相传。

(3)、主从之反客为主:
一主二从中说到过,主机挂了,从机原地待命,而反客为主就是主机挂了,从机上位了。要上位的从机执行如下命令即可:

slaveof no one

然后将另外一台从机挂在反客为主的这台机器下,那么原先的这两台从机就变成了一个新的一主一从体系。即使原先的主机回来了,也跟这个一主一从没关系了。

8、哨兵模式:
上面讲了主从的三种模式,那么哨兵又是什么鬼?说白了,哨兵模式就是上面“反客为主”的自动版。上面说的“反客为主”,主机挂了需要手动地将从机设置为主机,哨兵模式就不需要手动设置了。哨兵模式相当于有一个哨兵在巡逻,一旦发现主机挂了,就会以投票的方式立即选出新的老大。

  • 用法:首先新建一个名为sentinel.conf的文件,然后vi打开,在里面写一些配置,用来监控主机。配置如下:
#监控主机,主机名,主机ip,主机端口,1表示投票,谁的票数多就晋升为主机
sentinel monitor host6379 127.0.0.1 6379 1
#后台启动
daemonize yes

然后启动哨兵,在redis的src目录下执行如下命令:

./redis-sentinel /etc/sentinel.conf

启动成功后,再让主机挂掉,等几秒钟,就会发现已经自动选出新的主机了。


哨兵

可以看到,主机挂了之后,哨兵自动选出了6381为新的主机,6380也跟着6381混了。如果6379又复活了,它会继续担任6380和6381的老大呢还是与6380和6381没关系了?还是成了6381的小弟?看图说话:


哨兵

6379不再是主,成为了6381的小弟,6381反客为主成功。

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

推荐阅读更多精彩内容

  • 一、Redis高可用概述 在介绍Redis高可用之前,先说明一下在Redis的语境中高可用的含义。 我们知道,在w...
    空语阅读 1,593评论 0 2
  • Redis 初 级 精 讲 ...
    文思li阅读 1,011评论 0 0
  • NOSQL类型简介键值对:会使用到一个哈希表,表中有一个特定的键和一个指针指向特定的数据,如redis,volde...
    MicoCube阅读 3,958评论 2 27
  • 企业级redis集群架构的特点 海量数据 高并发 高可用 要达到高可用,持久化是不可减少的,持久化主要是做灾难恢复...
    lucode阅读 2,193评论 0 7
  • Redis 提供了多种不同级别的持久化方式: 了解 RDB 持久化和 AOF 持久化之间的异同是非常重要的, 以下...
    笑Skr人啊阅读 460评论 0 1