Redis

背景

随着互联网+大数据时代的来临,传统的关系型数据库已经不能满足中大型网站日益增长的访问量和数据量。这个时候就需要一种能够快速存取数据的组件来缓解数据库服务I/O的压力,来解决系统性能上的瓶颈

Redis

Redis是一个高性能的,开源的,C语言开发的,键值对存储数据的nosql数据库。

  • NoSQL:not only sql,泛指非关系型数据库 Redis/MongoDB/Hbase Hadoop
  • 关系型数据库:MySQL、oracle、SqlServer

数据库发展历史

  • 在互联网+大数据时代来临之前,企业的一些内部信息管理系统,一个单个数据库实例就能满足系统的需求
    • 单数据库
  • 随着系统访问用户的增多,数据量的增大,单个数据库实例已经满足不了系统的读取需求
    • 缓存(memcache)+单数据库实例
  • 缓存可以缓解系统的读取压力,但是数据量的写入压力持续增大
    • 缓存+主从数据库+读写分离
  • 数据量再次增大,读写分离以后,主数据库的写库压力出现瓶颈、
    • 缓存+主从数据库集群+读写分离+分库分表
  • 互联网+大数据时代来临,关系型数据库不能很好的存取一些并发性高,实时性高的,并且数据格式不固定的数据
    • nosql+主从数据库集群+读写分离+分库分表

NoSQL与SQL数据库比较

  • 适用场景不同:SQL数据库适合用于关系特别复杂的数据查询场景,nosql反之
  • 事务:SQL对事务的支持非常完善,而nosql基本不支持事务
  • 两者在不断的取长补短

Redis特性

  • Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用
  • Redis不仅仅支持简单的key-value类型的数据,同时还提供List,set等数据类型
  • Redis支持数据的备份

Redis作用

  • 快速存取

Redis安装与启动

  • Redis可以直接通过加载zip至任意位置进行使用
  • 以下命令需要进入Redis文件夹才可以进行
1
2
3
4
5
6
7
8
查看帮助命令
redis-server --help

启动服务
redis-server.exe

链接客户端
redis-cli.ex

配置文件

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
/etc/redis/redis.conf

当redis作为守护进程运行的时候,它会写一个 pid 到 /var/run/redis.pid 文件里面。
daemonize no

监听端口号,默认为 6379,如果你设为 0 ,redis 将不在 socket 上监听任何客户端连接。
port 6379

设置数据库的数目。
databases 16

根据给定的时间间隔和写入次数将数据保存到磁盘
下面的例子的意思是:
900 秒内如果至少有 1 个 key 的值变化,则保存
300 秒内如果至少有 10 个 key 的值变化,则保存
60 秒内如果至少有 10000 个 key 的值变化,则保存

save 900 1
save 300 10
save 60 10000

监听端口号,默认为 6379,如果你设为 0 ,redis 将不在 socket 上监听任何客户端连接。
port 6379

Redis默认只允许本地连接,不允许其他机器连接
bind 127.0.0.1

Redis数据库简单使用

1
2
3
4
5
DBSIZE      查看当前数据库的key数量
keys * 查看key的内容
FLUSHDB 清空当前数据库的key的数量
FLUSHALL 清空所有库的key(慎用)
exists key 判断key是否存在

Redis常用五大数据类型

Redis-string

  • 一个key对应一个value
  • 可以包含任何数据 但最大不超过512M
  • set/get/del/append/strlen
1
2
3
4
5
6
7
set  ---- 设置值
get ---- 获取值
mset ---- 设置多个值
mget ---- 获取多个值
append ---- 添加字段
del ---- 删除
strlen ---- 返回字符串长度
  • incr/decr/incrby/decrby
1
2
3
4
incr ---- 增加
decr ---- 减少
incrby ----- 制定增加多少
decrby ----- 制定减少多少
  • getrange/setrange
1
2
3
getrange ---- 获取指定区间范围内的值,类似between....and的关系
setrange ---- 代表从第几位开始替换,下脚本从零开始
从0 -1表示全部

Redis-list 单值多value

列表是简单的字符串列表,按照插入顺序排序,可以添加一个元素列表的头部(左边)或者尾部(右边)

它的底层实际是个链表

  • lpush/rpush/lrange
1
2
3
4
lpush/rpush/lrange ---- 从左/从右/获取指定长度
lpush list01 1 2 3 4 5 倒序排列
rpush list02 1 2 3 4 5 正序排列
lrange list01 0 -1 获取list01 中的所有值
  • lpop/rpop
1
2
3
lpop/rpop ---- 移除最左/最右
lpop list01 删除元素5
rpop list01 删除元素1
  • lindex,按照索引下标获得元素(从上到下)
1
2
lrange list01 0 -1
lindex list01 1
  • llen,求列表长度
1
llen list01
  • lrem key num value
1
2
删N个value
lrem list01 2 1 在list01中删除2个1
  • ltrim key
1
2
ltrim ---- 开始index结束index,截取指定范围的值后在赋值给key
ltrim list01 0 2 截取list01 从0到2的数据在赋值给list01
  • rpoplpush list1 list2 将list1中最后一个压入list2中第一位
1
2
3
lrange list01 0 -1
lrange list02 0 -1
rpoplpush list1 list2
  • lset key index value
1
lset list01 0 x     将list02中第一位换成x
  • linsert key before/after
1
linsert list01b  before x php  在x之前加字段php

Redis-Hash

hash是一个键值对集合

hash是一个string类型的field和value的映射表,hash特别适合存储对象

  • hset/hget/hmset/hmget/hgetall/hdel
1
2
3
4
5
6
7
设值/取值/设值多个值/取多个值/取全部值/删除值
hset user id 11
hget user id
hmset customer id 11 name juran age 26
hmget customer id name age 只返回相应的值
hgetall customer 返回全部
hdel user id 删除id
  • hlen 求hash长度
1
hlen customer
  • hexists key
1
2
hexists key value 在key里面的某个值
存在返回1 ,不存在返回0
  • hkeys/hvals
1
2
hkeys students
hvals students

Redis-set 不重复集合

set是string类型的无序集合

  • sadd/smembers/sismember
1
2
3
4
sadd/smembers/sismember ---- 添加/查看集合/查看是否存在
sadd set01 1 2 2 3 3 去掉重复添加
smembers set01 得到set01
sismember set01 1 如果存在返回1 不存在返回0
  • scard
1
2
scard ---- 获取集合里面的元素个数
scard set01
  • srem key value
1
2
3
srem ---- 删除集合中元素
srem set01 3
SMEMBERS set01 3已经被删除掉
  • srandmember key
1
2
3
srandmembe ---- 随机出几个数
sadd set02 1 2 3 4 5 6 7 8
srandmember set02 2
  • spop key
1
2
spop ---- 随机出栈
spop set01
  • smove key1 key2
1
smove set01 set03 2  将set01中的2 移动到set03中
  • 数学集合
1
2
3
4
5
6
7
8
sadd set01 1 2 3 4 5
sadd set02 1 2 3 a b
差集
SDIFF set01 set02 返回 4 5 在第一个set中不在第二个set中
交集
SINTER set01 set02 返回 1 2 3
并集
SUNION set01 set02 返回set01 set02 中的值 去掉重复

Redis-Zset

有序集合

  • zadd/zrange
1
2
3
zadd zset01 60 v1 70 v2 80 v3 90 v4 100 v5
zrange zset01 0 -1
带分数返回 withscores
  • zrangebyscore key start end
1
2
3
4
5
6
zrangebyscore key start end----根据开始结束来取值
zrangebyscore zset01 60 70

zrangebyscore zset01 60 (90 表示不包含90

zrangebyscore zset01 60 90 limit 1 2 从第一条开始截取2条
  • zrem key
1
2
zrem key value---- 某score下对应的value值,作用是删除元素
zrem zset01 v1
  • zcard/zcount key score 区间/zrank key values
1
2
3
zcard   求zset01 总条数
zcount zset01 60 90 求60-90个数
zrank zset01 v2 返回1 返回对应下角标,从0开始

Python操作Redis

redispy安装及连接

  • 安装
1
pip install redis
  • 连接
1
r = redis.StrictRedis(host='localhost',port=6379,db=0)

字符串操作

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
import redis

class TestString(object):
def __init__(self):
self.r = redis.StrictRedis(host='192.168.75.130',port=6379)
设置值
def test_set(self):
res = self.r.set('user1','juran-1')
print(res)
取值
def test_get(self):
res = self.r.get('user1')
print(res)
设置多个值
def test_mset(self):
d = {
'user2':'juran-2',
'user3':'juran-3'
}
res = self.r.mset(d)
取多个值
def test_mget(self):
l = ['user2','user3']
res = self.r.mget(l)
print(res)
删除
def test_del(self):
self.r.delete('user2')

列表相关操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class TestList(object):
def __init__(self):
self.r = redis.StrictRedis(host='192.168.75.130',port=6379)
插入记录
def test_push(self):
res = self.r.lpush('common','1')
res = self.r.rpush('common','2')
# res = self.r.rpush('jr','123')
弹出记录
def test_pop(self):
res = self.r.lpop('common')
res = self.r.rpop('common')
范围取值
def test_range(self):
res = self.r.lrange('common',0,-1)
print(res)

集合相关操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class TestSet(object):
def __init__(self):
self.r = redis.StrictRedis(host='192.168.75.130', port=6379)
添加数据
def test_sadd(self):
res = self.r.sadd('set01','1','2')
lis = ['Cat','Dog']
res = self.r.sadd('set02',lis)
删除数据
def test_del(self):
res = self.r.srem('set01',1)
随机删除数据
def test_pop(self):
res = self.r.spop('set02')

Hash相关操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class TestHash(object):
def __init__(self):
self.r = redis.StrictRedis(host='192.168.75.130', port=6379)

批量设值
def test_hset(self):
dic = {
'id':1,
'name':'huawei'
}
res = self.r.hmset('mobile',dic)
批量取值
def test_hgetall(self):
res = self.r.hgetall('mobile')
判断是否存在 存在返回1 不存在返回0
def test_hexists(self):
res = self.r.hexists('mobile','id')
print(res)

Scrapy-分布式

什么是scrapy_redis

1
scrapy_redis:Redis-based components for scrapy

工作流程

与正常scrapy工作不同的是

  • pipeline住的数据会默认存储到Redis
  • 通过Redis实现调度器的队列和指纹集合

scrapy_redis下载

1
2
clone github scrapy_redis源码文件
git clone https://github.com/rolando/scrapy-redis.git

scrapy_redis中的settings文件

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
# Scrapy settings for example project
#
# For simplicity, this file contains only the most important settings by
# default. All the other settings are documented here:
#
# http://doc.scrapy.org/topics/settings.html
#
SPIDER_MODULES = ['example.spiders']
NEWSPIDER_MODULE = 'example.spiders'

USER_AGENT = 'scrapy-redis (+https://github.com/rolando/scrapy-redis)'

DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" # 指定那个去重方法给request对象去重
SCHEDULER = "scrapy_redis.scheduler.Scheduler" # 指定Scheduler队列
SCHEDULER_PERSIST = True # 队列中的内容是否持久保存,为false的时候在关闭Redis的时候,清空Redis
#SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.SpiderPriorityQueue"
#SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.SpiderQueue"
#SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.SpiderStack"

ITEM_PIPELINES = {
'example.pipelines.ExamplePipeline': 300,
'scrapy_redis.pipelines.RedisPipeline': 400, # scrapy_redis实现的items保存到redis的pipline
}

LOG_LEVEL = 'DEBUG'

# Introduce an artifical delay to make use of parallelism. to speed up the
# crawl.
DOWNLOAD_DELAY = 1

scrapy_redis运行

1
2
3
4
allowed_domains = ['dmoztools.net']
start_urls = ['http://www.dmoztools.net/']

scrapy crawl dmoz
运行结束后redis中多了三个键
1
2
3
dmoz:requests   存放的是待爬取的requests对象  获取过程是pop操作即获取一个删除一个
dmoz:item 爬取到的信息 在pipeline中开启RedisPipeline才会存入
dmoz:dupefilter 爬取的requests的指纹 存放的是已经进入schedule队列的request对象的指纹,指纹默认由请求方法,url和请求体组成

普通Scrapy爬虫改写为分布式(scrapy_redis)爬虫

  • 创建Scrapy项目

  • 明确爬取目标

  • 创建爬虫项目

  • 保存数据

  • 改写分布式爬虫

    • 改写爬虫文件

      • 导入模块
      • 继承类
      • 把start_urls改为redis_key
    • 改写配置文件

      1
      2
      3
      4
      5
      6
      7
      8
      # 指定哪个方法去重给request对象 DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" # 指定Scheduler的队列 
      SCHEDULER = "scrapy_redis.scheduler.Scheduler"
      SCHEDULER_PERSIST = True

      ITEM_PIPELINES = {
      'example.pipelines.ExamplePipeline': 300,
      'scrapy_redis.pipelines.RedisPipeline': 400,
      }

爬取当当网案例

  • 主文件
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import scrapy
from copy import deepcopy
from scrapy_redis.spiders import RedisSpider #导入模块
'''
- 1 改写爬虫文件
- 导入模块
- 继承类
- 把start_urls ---> redis_key
- 2 改写配置文件

'''
class DangdangSpider(RedisSpider): #继承类
name = 'dangdang'
allowed_domains = ['dangdang.com']
# start_urls = ['http://book.dangdang.com/']
redis_key = 'dangdang' #把start_urls ---> redis_key 运行url填入数据库

def parse(self, response):
div_list = response.xpath("//div[@class='con flq_body']/div")

for div in div_list:
item = {}
# item['b_cate'] = div.xpath("./dl/dt//text()").extract()
# if item['b_cate']:
# item['b_cate'] = [i.strip() for i in item['b_cate'] if len(i.strip()) > 0]
# else:
# item['b_cate'] = ''
# 获取大分类
item['b_cate'] = div.xpath("./dl/dt//text()").extract()
# 处理格式
item['b_cate'] = [i.strip() for i in item['b_cate'] if len(i.strip())>0]

dl_list = div.xpath(".//dl[@class='inner_dl']")
for dl in dl_list:
# 获取中分类
item['m_cate'] = dl.xpath('./dt//text()').extract()
item['m_cate'] = [i.strip() for i in item['m_cate'] if len(i.strip()) > 0]

a_list = dl.xpath('./dd/a')
for a in a_list:
# 获取小分类
item['s_cate'] = a.xpath('./text()').extract_first()
# 小分类的url
item['s_href'] = a.xpath('./@href').extract_first()

if item['s_href'] is not None:
yield scrapy.Request(
url=item['s_href'],
callback=self.parse_book_list,
meta={'item':deepcopy(item)}
)


def parse_book_list(self,response):

item = response.meta.get('item')
li_list = response.xpath("//ul[@class='list_aa ']/li")
for li in li_list:
# 图片的url
item['book_img'] = li.xpath('./a[@class="img"]/img/@src').extract_first()
if item['book_img'] == 'images/model/guan/url_none.png':
item['book_img'] = li.xpath('./a[@class="img"]/img/@data-original').extract_first()
# 获取图片的名字
item['book_name'] = li.xpath('./p[@class="name"]/a/@title').extract_first()

# print(item)
yield item
  • settings
1
2
3
4
5
6
7
8
9
10
11
12
13
14
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
SCHEDULER_PERSIST = True

DEFAULT_REQUEST_HEADERS = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36'
}

ITEM_PIPELINES = {
'book.pipelines.BookPipeline': 300,
'scrapy_redis.pipelines.RedisPipeline': 400,
}