学习记录

他山之石,可以攻玉

引言

前两天写了个NodeJs爬虫,很简单那种。就是爬取某个网站的首页解析所有的Url然后再爬取内页。这里就带来了一个问题。众所周知,Js是单线程异步的,并以此为卖点。可是在这个场景就有一个很明显的问题。
比如首页有30个Url它会一次性解析30个Url然后同时去请求,失败率不可谓不高。类似的场景我曾经使用过用数组来模拟栈用于控制并发,然实在不能说很优雅,各种判断,轮训,时间长了再去看不合心意,于是便想到Redis。
这是一个NoSql数据库,经常用来做缓存,因为是存在内存里的效率很高,当然也支持存进硬盘做持久化,我看上它是有一个事务,能否用这个事务来做并发控制呢?就是我来学习它的初衷,毕竟既能解决缓存又可以做并发控制岂不美哉?

初探

  1. 安装
    Redis的安装非常简单,以Linux为例:

     前往首页选择最新稳定版下载(官网下载地址:https://redis.io/download)
          $ wget http://download.redis.io/releases/redis-4.0.9.tar.gz
          $ tar xzf redis-4.0.9.tar.gz
          $ cd redis-4.0.9
          $ make
      好了装完了  
  2. 使用

        $ src/redis-server //开启本地服务器
        $ src/redis-cli //开启本地客户端 
        redis> set foo bar
        OK
        redis> get foo
        "bar"
        以上是官网示例
        默认端口:6379 可以通过 --port 来设置
        cli 可以通过 -h 指定host地址,-p来指定端口
        然后为了方便我们可以。
        ln -s  /home/pi/redis-4.0.9/src/redis-server  /usr/bin/redis-server
        ln -s  /home/pi/redis-4.0.9/src/redis-cli /usr/bin/redis-cli
    //注意一点,默认情况下Redis只能本地访问,设置密码以后才能远端访问。
  3. 基本概念
    数据结构
    Redis有五种数据结构:String,Hash,List,Set和zset。
    String(字符串)
    简单的键值对:

    SET name “Redis”
    GET name
    //输出 Redis

    Hash(哈希)
    是个键值集合,存数据的方式,当作Js的对象来理解似乎没有太大问题。
    这个估计最常用

    HMSET obj name “Redis” state “Studying” 
    HGGET obj name
    //输出Redis
    HGET obj
    //输出 Redis
    //Studying

    List(列表)
    字符串列表,按照输入顺序排序,类比Js的话,就是使用push和unpush的数组。
    我觉得翻译成队列更合适。

     lpush runoob redis
     lpush runoob mongodb
     lpush runoob rabitmq
     lrange runoob 0 10
    //输出
        1) "rabitmq"
         2) "mongodb"
         3) "redis"
    

    Set(集合)
    学术性的说法叫:String的无序集合。
    在我看来..相当于先创建一个空数组,然后一个值一个值往里加,与上面的区别似乎是这是个哈系表?。

    sadd runoob 
    sadd runoob redis
    sadd runoob mongodb
    sadd runoob rabitmq
    sadd runoob rabitmq
    smembers runoob
     //输出
     1) "redis"
     2) "rabitmq"
     3) "mongodb"

    zset(有序集合)

    自带排序的集合..
    学术性的说法:不允许重复的,每一个元素关联一个double类型数的String类型元素集合。每个元素虽然不能重复,但是被用来排序的double值是可以重复的。
    通俗一点就是,下标只用来排序且允许重复,每个成员都是字符串且唯一的数组....

         zadd runoob 0 redis
         zadd runoob 0 mongodb
         zadd runoob 0 rabitmq
         zadd runoob 0 rabitmq
         ZRANGEBYSCORE runoob 0 1000
         1) "mongodb"
         2) "rabitmq"
         3) "redis"
  4. 基本操作
    上面每一种都介绍一下的话,篇幅太长,我重点看看Hash的..其它估计也都差不多,举一反三。只要会增删改查感觉基本的应用就没问题了。

      增:HMSET key name1 value1 name2 value2 ....
      删:HDEL key [name value] //允许删除整个key 或者只删除key里面的某一个值
      改:HMSET key name1 value1 name2 value2 .... //直接覆盖,和Js的对象一样..
      查:HMGET key name //获取key下某个字段的值
       HGETALL key   //获取某key所有的字段和值

    一些花样

    HEXISTS key name  //某Key 对应字段是否存在
    HSETNX key field value //安全添加只有字段不存在的时候才会添加
    HKEYS key //获取所有的关键字
    HVALS key //获取所有的值

    5.事务
    恩,到重点了,我原本是想用这个做并发控制来着。只看描述似乎是没问题的。
    而且事务似乎有Watch这个方法..检测某值如果改变则取消...
    emmm..高并发的抢购活动似乎很有用,问题是,我可能需要一个,改变值才去执行的方法。
    Redis似乎也有发布订阅....emmmm,太繁琐了..我可能不太需要一个观察者..
    实际去阅读文档发现,这个事务执行的是Redis命令...我想想...

    读取所有Url
    取一部分用事务存入Reds。
    再一条一条取出来下载。
    下完了再执行这个步骤..
    emmmm...我为什么不新建一个数组?

    好吧,言归正传。
    所谓事务
    Redis 事务可以一次执行多个命令, 并且带有以下两个重要的保证:

  • 批量操作在发送 EXEC 命令前被放入队列缓存。
  • 收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行。
  • 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。
  • 一个事务从开始到执行会经历以下三个阶段:

三个阶段:

    1. 开始事务。  
    2. 命令入队。  
    3. 执行事务。  

注意点:
Redis的命令执行是原子性的,即不成功就失败,失败会回滚状态,不存在中间态。颇有“无胜利,毋宁死”的感觉。
但是事务不一样,虽然Redis的命令是原子性的,但是Redis的事务不是,也就是说,如果一组事务有几个失败了,成功的依旧不会回滚。
例子:

redis 127.0.0.1:6379> MULTI
OK

redis 127.0.0.1:6379> SET book-name "Mastering C++ in 21 days"
QUEUED

redis 127.0.0.1:6379> GET book-name
QUEUED

redis 127.0.0.1:6379> SADD tag "C++" "Programming" "Mastering Series"
QUEUED

redis 127.0.0.1:6379> SMEMBERS tag
QUEUED

redis 127.0.0.1:6379> EXEC
1) OK
2) "Mastering C++ in 21 days"
3) (integer) 3
4) 1) "Mastering Series"
   2) "C++"
   3) "Programming"
  1. 其它
    除了上面这些,Redis还有发布订阅,脚本,等其它知识点,只是暂时似乎用不上,所以,在此略过不表。

    在Node.js中使用Redis

    Node.js有一个Redis库,直接

    npm install redis;
    node
    var redis = require('redis')
    var client = redis.createClient(port, host); 

    这里就能拿到一个可以执行Redis的对象。
    用法示例:

    //原命令 hmset key name1 value1 name2 value2...
    //在Js中
    var mainkey = key;
    vae setOBj={
     name1:value1,
     name2:value2,
     ...
    }
    client.hmset(mainKey,setObj,(err,res)=>{
         if(err){
           console.log(err);
          return;
         }
         console.log(res);//正常这里会返回OK
         client.quit(); //单次操作建议退出,不退出超时也会断开。
     });
    
    //原命令 hgetall key
    var mainid = key;
    client.HGETALL(mainid,(err,res)=>{
          if(err){
              console.log(err);
              return;
          }
         result=res;//这里返回的是对象。
         client.quit();
      
    });

    值得一提的是,hmset也好,HMSET也罢,它都认识..很方便。
    需要注意的是,这里无论写入还是读取都是异步的,写入还好,如果是读取,恐怕需要注意使用async,await,和Promise来做同步操作。
    这里我只是举了两个刚用到的列子,其它还有很多,github上写的很详细注意返回数据的时机就可以了。

    结语

    本以为利用Redis的事务可以做爬虫的并发控制,最后发现,并不能,如果它能在事务中执行下载的话,倒是可以做图片下载的并发控制..
    还有待研究,这篇文章作为完整的入门来说并不合格,我仅仅只是发现一个问题,然后去解决,记录下来了解决过程罢了。
    Node.js的Redis库暂时没看见中文文档,示例中有一些用的到的地方,看的有点费劲,程序员学好英语还是非常必要的。
    关于Node.js中使用Redis等待返回,官网有个示例,不过,我是之后才去看到,所以,我用的自己的方法,但是,这个方法很蠢,一个请求创建了两个Promis,还有待优化。
    简单介绍下:在Api中使用Async 创建一个Promis,await,调用一个带Async的函数,函数中调用Client的方法,这里再来一个Promise,await。
    去掉Api中的Async Promise await的话,内层调用Client的函数会直接返回一个Promise即使没有读到return,推测是Client函数中的方法做的,经过一遍一遍的测试,最后变成了如上所述的结构。
    //2018/5/30
    修正这一段描述,今天看ES6的入门,这里的Promise是async返回的,猛然想到,如果去掉async是没有promise返回的,当时忽略了这一点。

做完后发现github上的示例有一个推荐写法....emmmmmm...
查文档很重要..好..那么就这样。

Comment

This is just a placeholder img.