非关系型数据库redis和memcache

redis介绍:
windows:
https://www.cnblogs.com/liuqingzheng/articles/9833534.html
linux:
https://lupython.gitee.io/2017/05/05/Redis学习目录/
中文官网:www.redis.cn

访问量多的时候我们需要多个服务器做负载均衡,一种方式叫轮循, 另一种叫权重,
当请求数据库压力过大时,我们需要做主从(一般是一主多从),
当主从仍然无法满足我们需求时.我们需要加缓存(以前用的memcache,redis出现以后用redis),
加缓存以后请求数据库先从缓存中查找,找不到才从数据库中查找,在数据库中拿到以后要在缓存中放一份,以防止再次访问

一.安装redis

Redis-x64-3.2.100(1).msi: redis安装包
redis-desktop-manager-0.9.3.817.exe:redis管理工具

#安装介绍网站
https://www.cnblogs.com/liuqingzheng/p/9831331.html

redis官方没有提供windows安装包,建议安装到linux中
安装到linux中我们需要关注下面几个文件
redis-benchmark :压力测试
(apache有一个ab工具可以做压力测试,模拟流量大时网站的承受能力
运行:在windows系统下,打开cmd命令切换到apache安装目录下
输入命令:ab -n 800 -c 800 http://192.168.0.10/
-n发出800个请求,-c模拟800并发,相当于800人同时访问,后面是测试url
ab -t 60 -c100 http://192.168.0.10/
在60秒内发送请求,一次100个请求)

redis-check-aof :检查aof日志
redis-check-rdb :检查rdb日志 redis中数据持久化的方式:aof和rdb
redis-sentinel :哨兵 做redis集群时会用到,有sentinel当主redis出现故障的时候会自动切换到从redis
redis-cli    :客户端连接
redis-server :启动redis服务 默认端口6379 修改:redis.conf

二.简介

'''
-mysql,oracle:关系型数据库
-redis,mongodb:非关系型数据库/nosql
    -redis储存在内存中,redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步
    -mongodb储存在硬盘上

-redis一般用来
   -数据库 -做缓存(最重要用途) -session数据 -游戏排行 -对速度要求比较高的数据储存 -做消息队列 -redis是key-value的存储,支持持久化,像py中的字典,有五大数据类型: -字符串(string) -列表(list) -字典(hash) -集合(set) -有序集合(order_set) redis={ k1:'123', 字符串 k2:[1,2,3,4], 列表/数组 k3:{1,2,3,4} 集合 k4:{name:lqz,age:12} 字典/哈希表 k5:{('lqz',18),('egon',33)} 有序集合 } #redis支持上述五大数据类型,但是它只支持一层 -比较redis和Memcached -redis 支持5大数据类型 -redis支持持久化 -单线程,单进程,速度是非常快 -Memcached不能持久化,只支持字符串 -存redis的token 三种方案: -request.user 是字典或者用户id -request.user 就是user对象,在redis中存的value值是pickle之后的字符串 -request.user 就是user对象,在redis中存的value值是json格式数据

三.python操作redis

1.安装redis模块
2.简单操作
'''
import redis
#拿到redis的连接
conn=redis.Redis(host='127.0.0.1',port=6379,password=12345)
#插入字符串数据
conn.set('age','18')
#获取数据
name=conn.get('name')
#redis中存的都是byte格式
print(name)
'''

1.redis连接池

redis-py使用connection pool来管理对一个redis server的所有连接,避免每次建立、释放连接的开销。默认,每个Redis实例都会维护一个自己的连接池。可以直接建立一个连接池,然后作为参数Redis,这样就可以实现多个Redis实例共享一个连接池

#必须是单例
 -实现单例的方式
    ①当其以模块导入实现单例,这是python中一种天然的单例方式
    ②__new__实现单例
    ③装饰器来控制实现单例
'''
#模块导入实现单例
-新建一个模块
import redis                    
POOL=redis.ConnectionPool(host='127.0.0.1',port=6379,password=12345,max_connections=1000) -使用连接池 #导入池子 from conn_pool import POOL # 每次执行这句话,从池子中取一个链接 conn=redis.Redis(connection_pool=POOL)
'''

2.redis之字符串操作

#重点掌握
get set mget mset incr decr append

#String操作,redis中的String在在内存中按照一个name对应一个value来存储

1.set(name, value, ex=None, px=None, nx=False, xx=False)
在Redis中设置值,默认,不存在则创建,存在则修改
参数:
     ex,过期时间(秒)
     px,过期时间(毫秒)
     nx,如果设置为True,则只有name不存在时,当前set操作才执行,值存在,就修改不了,执行没效果
     xx,如果设置为True,则只有name存在时,当前set操作才执行,值存在才能修改,值不存在,不会设置新值

2.setnx(name, value) #设置值,只有name不存在时,执行设置操作(添加),如果存在,不会修改

3.setex(name, time, value) # 设置值
# 参数:
    # time,过期时间(数字秒 或 timedelta对象)

4.psetex(name, time_ms, value) # 设置值
# 参数:
    # time_ms,过期时间(数字毫秒 或 timedelta对象

5.mset(*args, **kwargs) #批量设置值
如:
    mset(k1='v1', k2='v2')
    或
    mset({'k1': 'v1', 'k2': 'v2'})

6.get(name) #获取值

7.mget(names, *args) #批量获取
如:
    mget('k1', 'k2')
    或
    mget(['k3', 'k4'])

8.getset(name, value) #设置新值并获取原来的值

9.getrange(name, start, end)
# 获取子序列(根据字节获取,非字符)
# 参数:
    # name,Redis 的 name
    # start,起始位置(字节)
    # end,结束位置(字节)
# 如: "henry" ,0-2表示 "hen" 前闭后闭,全闭区间

10.setrange(name, offset, value)
# 修改字符串内容,从指定字符串索引开始向后替换(新值太长时,则向后添加)
# 参数:
    # offset,字符串的索引,字节(一个汉字三个字节)
    # value,要设置的值

11.setbit(name, offset, value)
# 对name对应值的二进制表示的位进行操作
 
# 参数:
    # name,redis的name
    # offset,位的索引(将值变换成二进制后再进行索引)
    # value,值只能是 1 或 0
 
# 注:如果在Redis中有一个对应: n1 = "foo",
        那么字符串foo的二进制表示为:01100110 01101111 01101111
    所以,如果执行 setbit('n1', 7, 1),则就会将第7位设置为1,
        那么最终二进制则变成 01100111 01101111 01101111,即:"goo"

12.getbit(name, offset)
# 获取name对应的值的二进制表示中的某位的值 (0或1)

13.bitcount(name, start=None, end=None)
# 获取name对应的值的二进制表示中 1 的个数
# 参数:
    # name,Redis的name
    # start,位起始位置
    # end,位结束位置

14.bitop(operation, dest, *keys)
# 获取多个值,并将值做位运算,将最后的结果保存至新的name对应的值
 
# 参数:
    # operation,AND(并) 、 OR(或) 、 NOT(非) 、 XOR(异或)
    # dest, 新的Redis的name
    # *keys,要查找的Redis的name
 
# 如:
    bitop("AND", 'new_name', 'n1', 'n2', 'n3')
    # 获取Redis中n1,n2,n3对应的值,然后讲所有的值做位运算(求并集),然后将结果保存 new_name 对应的值中

15.strlen(name)
# 返回name对应值的字节长度(一个汉字3个字节)

16.incr(self, name, amount=1) #amount默认为1 用于浏览量等
# 自增 name对应的值,当name不存在时,则创建name=amount,否则,则自增。
 
# 参数:
    # name,Redis的name
    # amount,自增数(必须是整数)
 
# 注:同incrby

17.incrbyfloat(self, name, amount=1.0)
# 自增 name对应的值,当name不存在时,则创建name=amount,否则,则自增。
 
# 参数:
    # name,Redis的name
    # amount,自增数(浮点型)

18.decr(self, name, amount=1) #amount默认为1
# 自减 name对应的值,当name不存在时,则创建name=amount,否则,则自减。
 
# 参数:
    # name,Redis的name
    # amount,自减数(整数)

19.append(name, value)
# 在redis name对应的值后面追加内容
 
# 参数:
    name, redis的name
    value, 要追加的字符串

20.自定义增量迭代
# 由于redis类库中没有提供对列表元素的增量迭代,如果想要循环name对应的列表的所有元素,那么就需要:
    # 1、获取name对应的所有列表
    # 2、循环列表
# 但是,如果列表非常大,那么就有可能在第一步时就将程序的内容撑爆,所有有必要自定义一个增量迭代的功能:
def scan_list(name,count=1000) #count为默认一次取出数据的个数
    cursor=0
    while True:
        if cursor >= re.llen(name):
            break
        ret = re.lrange(name,cursor,count+cursor-1)
        cursor+=count
        for i in ret:
            yield i
#使用
for i in scan_list('l1',100):#一次取100条数据
    print(i)

应用场景:在登录时可以做频率控制
每次访问判断
if conn.get('cnt') > 5:
  set cnt 0 EX 900 #设置cnt超时时间900s
else:
  conn.incr('cnt')

3.redis之列表操作

#List操作,redis中的List在在内存中按照一个name对应一个List来存储。

1.r.lpush(name,values)
# 在name对应的list中添加元素,每个新的元素都添加到列表的最左边
 
# 如:
    # r.lpush('l1', 1,2,3)
    # 保存顺序为: 3,2,1
    
    # r.lpush('l2',*[1,2,3])
    #保持顺序为: 3,2,1
 
# 扩展:
    # rpush(name, values) 表示从右向左操作
    
2.r.lpushx(name,value)
# 在name对应的list中添加元素,只有name已经存在时,值添加到列表的最左边,如果name不存在不进行任何操作
 
# 更多:
    # rpushx(name, value) 表示从右向左操作
    
3.r.llen(name)
# name对应的list元素的个数

4.r.linsert(name, where, refvalue, value))
# 在name对应的列表的某一个值前或后插入一个新值
 
# 参数:
    # name,redis的name
    # where,BEFORE或AFTER(小写也可以)
    # refvalue,标杆值,即:在它前后插入数据(如果存在多个标杆值,以找到的第一个为准)
    # value,要插入的数据
    # r.linsert('l1','BEFORE','3','p') 在l1列表中3的前面加一个p

5.r.lset(name, index, value)
# 对name对应的list中的某一个索引位置重新赋值
 
# 参数:
    # name,redis的name
    # index,list的索引位置
    # value,要设置的值
    
6.r.lrem(name, num, value)
# 在name对应的list中删除指定的值
 
# 参数:
    # name,redis的name
    # num,  num=0,删除列表中所有的指定值;
           # num=2,从前到后,删除2个;
           # num=-2,从后向前,删除2个
    # value,要删除的值

7.r.lpop(name)
# 在name对应的列表的左侧获取第一个元素并在列表中移除,返回值则是第一个元素
 
# 更多:
    # rpop(name) 表示从右向左操作

8.r.lindex(name, index)
#在name对应的列表中根据索引获取列表元素

9.r.lrange(name, start, end)
# 在name对应的列表分片获取数据
# 参数:
    # name,redis的name
    # start,索引的起始位置
    # end,索引结束位置  print(r.lrange('aa',0,r.llen('aa')))  
    #前闭后闭区间

10.r.ltrim(name, start, end)
# 在name对应的列表中移除没有在start-end索引之间的值
# 参数:
    # name,redis的name
    # start,索引的起始位置
    # end,索引结束位置(大于列表长度,则代表不移除任何)
    #前闭后闭区间
    
11.r.rpoplpush(src, dst)
# 从一个列表取出最右边的元素,同时将其添加至另一个列表的最左边
# 参数:
    # src,要取数据的列表的name
    # dst,要添加数据的列表的name
    
12.blpop(name, timeout)
# 从左到右去pop对应列表的元素
 
# 参数:
    # name,redis的name
    # timeout,超时时间,当所有列表的元素获取完之后,将会阻塞,然后等待列表内有数据的时间(秒), 0 表示永远阻塞,默认是0
 
# 更多:
    # r.brpop(keys, timeout),从右向左获取数据
爬虫实现简单分布式:多个url放到列表里,往里不停放URL,程序循环取值,但是只能一台机器运行取值,可以把url放到redis中,多台机器从redis中取值,爬取数据,实现简单分布式,一个负责爬连接,一个负责取数据存数据库

13.brpoplpush(src, dst, timeout=0)
# 从一个列表的右侧移除一个元素并将其添加到另一个列表的左侧
 
# 参数:
    # src,取出并要移除元素的列表对应的name
    # dst,要插入元素的列表对应的name
    # timeout,当src对应的列表中没有数据时,阻塞等待其有数据的超时时间(秒),0 表示永远阻塞

4.redis之字典(Hash)操作

#Hash操作,redis中Hash在内存中的存储格式
name -> k1->v1
        k2->v2
        k3->v3

1.hset(name, key, value)
# name对应的hash中设置一个键值对(不存在,则创建;否则,修改)
 
# 参数:
    # name,redis的name
    # key,name对应的hash中的key
    # value,name对应的hash中的value
#r.hset('n1','name','henry')
# 注:
    # hsetnx(name, key, value),当name对应的hash中不存在当前key时则创建(相当于添加),存在不进行任何操作

2.hmset(name, mapping)
# 在name对应的hash中批量设置键值对
 
# 参数:
    # name,redis的name
    # mapping,字典,如:{'k1':'v1', 'k2': 'v2'}
 
# 如:
    # r.hmset('xx', {'k1':'v1', 'k2': 'v2'})

3.hget(name,key)
# 在name对应的hash中根据key获取value

4.hmget(name, keys)
# 在name对应的hash中获取多个key的值
 
# 参数:
    # name,reids对应的name
    # keys,要获取key集合,如:['k1', 'k2', 'k3'] 
       # 第一个参数为name,后面也可以直接写'k1','k2','k3'
     
# 如:
    # r.mget('xx', ['k1', 'k2'])
    #
    # print(r.hmget('xx', 'k1', 'k2'))
    
5.hgetall(name)
# 把字典中的所有键-值对取出来,如果存储的数据量过大,尽量慎用
# 获取name对应hash的所有键值
print(re.hgetall('xxx').get(b'name'))

6.hlen(name)
# 获取name对应的hash中键值对的个数

7.hkeys(name)
# 获取name对应的hash中所有的key的值

8.hvals(name)
# 获取name对应的hash中所有的value的值

9.hexists(name, key)
# 检查name对应的hash是否存在当前传入的key

10.hdel(name,*keys)
# 将name对应的hash中指定key的键值对删除,返回值为你删除了几个键值对
print(re.hdel('xxx','sex','name'))

11.hincrby(name, key, amount=1)
# 自增name对应的hash中的指定key的值,不存在则创建key=amount
# 参数:
    # name,redis中的name
    # key, hash对应的key
    # amount,自增数(整数)
re.hincrby('n1','age',amount=1) #n1中的age字段的值自增1,不存在则创建age字段值为1
    
12.hincrbyfloat(name, key, amount=1.0)
# 自增name对应的hash中的指定key的值,不存在则创建key=amount
 
# 参数:
    # name,redis中的name
    # key, hash对应的key
    # amount,自增数(浮点数)
 
# 自增name对应的hash中的指定key的值,不存在则创建key=amount

13.hscan(name, cursor=0, match=None, count=None)
# 增量式迭代获取,对于数据大的数据非常有用,hscan可以实现分片的获取数据,并非一次性将数据全部获取完,从而放置内存被撑爆
 
# 参数:
    # name,redis的name
    # cursor,游标(基于游标分批取获取数据)
    # match,匹配指定key,默认None 表示所有的key
    # count,每次分片最少获取个数,默认None表示采用Redis的默认分片个数
    #返回有两个值,一个是游标的位置,一个是取到的值
 
# 如:
    # 第一次:cursor1, data1 = r.hscan('xx', cursor=0, match=None, count=None)
    # 第二次:cursor2, data1 = r.hscan('xx', cursor=cursor1, match=None, count=None)
    # ...
    # 直到返回值cursor的值为0时,表示数据已经通过分片获取完毕

14.hscan_iter(name, match=None, count=None)
# 利用yield封装hscan创建生成器,实现分批去redis中获取数据
 
# 参数:
    # match,匹配指定key,默认None 表示所有的key
    # count,每次分片最少获取个数,默认None表示采用Redis的默认分片个数
 
# 如:
    # for item in r.hscan_iter('xx'):
    #     print item

5.redis之集合(set)操作

linux使用下
网址:
https://lupython.gitee.io/2017/05/05/Redis%E5%AD%A6%E4%B9%A0%E7%9B%AE%E5%BD%95/

sinter s1 s2 s3  :求s1,s2,s3三个集合的交集
sunion s1 s2 s3 :求s1,s2,s3三个集合的并集
sdiff  s1 s2 s3 :求s1与s2,s3的差集 s1-s2-s3
应用场景:微博共同关注 sinter

6.redis之有序集合(order_set)操作

linux
1.zadd s1 score1 value1 score2 value2 ..
zadd stu 18 lily 19 hmm 20 lilei 21

2.ZRANGE  key start stop [WITHSCORES]
把集合排序后,返回名次[start,stop]的元素
默认是升续排列 
Withscores 是把score也打印出来
ZRANGE stu 0 -1   (从头开始到结束全拿出来)
ZRANGE stu 0 -1  WITHSCORES

3.zrank key member
查询member的排名(升序 0名开始)
ZRANK stu lily #0
ZRANK stu hmm #1
ZRANK stu lilei #2

4.zrevrank key memeber
查询member的排名(降序 0名开始)

5.zcard key
返回元素个数

7.其它操作

1.delete(*names)
# 根据删除redis中的任意数据类型

2.exists(name)
# 检测redis的name是否存在,返回值为存在的个数

3.keys(pattern='*')
# 根据模型获取redis的name
 
# 更多:
    # KEYS * 匹配数据库中所有 key 。
    # KEYS h?llo 匹配 hello , hallo 和 hxllo 等。
    # KEYS h*llo 匹配 hllo 和 heeeeello 等。
    # KEYS h[ae]llo 匹配 hello 和 hallo ,但不匹配 hillo 

4.expire(name ,time)
# 为某个redis的某个name设置超时时间

5.rename(src, dst)
# 对redis的name重命名为

6.move(name, db))
# 将redis的某个值移动到指定的db下

7.randomkey()
# 随机获取一个redis的name(不删除)

8.type(name)
# 获取name对应值的类型

9.scan(cursor=0, match=None, count=None)
scan_iter(match=None, count=None)
# 用于增量迭代获取key

8.管道

redis-py默认在执行每次请求都会创建(连接池申请连接)和断开(归还连接池)一次连接操作,如果想要在一次请求中指定多个命令,则可以使用pipline实现一次请求指定多个命令,并且默认情况下一次pipline 是原子性操作。
#注意:关系型数据库存在事务,非关系型数据库不存在事务,但是我们可以通过管道模拟出事务

import redis
 
pool = redis.ConnectionPool(host='10.211.55.4', port=6379)
r = redis.Redis(connection_pool=pool)

pipe = r.pipeline(transaction=True)
pipe.multi()
pipe.set('name', 'alex')
pipe.set('role', 'sb')
 
pipe.execute()

9.Django中使用redis

方式一

建立redis_pool.py
import redis
POOL = redis.ConnectionPool(host='127.0.0.1', port=6379,password='1234',max_connections=1000)

视图函数中使用:
import redis
from django.shortcuts import render,HttpResponse
from utils.redis_pool import POOL

def index(request):
    conn = redis.Redis(connection_pool=POOL)
    conn.hset('kkk','age',18)

    return HttpResponse('设置成功')
def order(request):
    conn = redis.Redis(connection_pool=POOL)
    conn.hget('kkk','age')

    return HttpResponse('获取成功')

方式二

安装django-redis模块
pip3 install django-redis

setting里配置:
CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
            "CONNECTION_POOL_KWARGS": {"max_connections": 100}
            # "PASSWORD": "123",
        }
    },
    "aa": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
            "CONNECTION_POOL_KWARGS": {"max_connections": 100}
            # "PASSWORD": "123",
        }
    },
}

视图函数:
from django_redis import get_redis_connection
conn = get_redis_connection('default')  #可以选择自己要操作的服务器,默认default
print(conn.hgetall('xxx'))

10.常见应用场景

网址:
https://lupython.gitee.io/2017/05/05/Python操作redis及其应用/
1.统计页面点击数
论坛每个帖子都要记录点击次数,而点击次数比回帖的次数的多得多。如果使用关系数据库来存储点击,可能存在大量的行级锁争用。所以,点击数的增加使用redis的INCR命令最好不过了

2.如何保存一个对象数据

3.社交圈子数据
微博共同关注

4.反垃圾系统
获取某段时间所有数据排重值,这个使用Redis的set数据结构最合适了,只需要不断地将数据往set中扔就行了,set意为集合,所以会自动去重

5.Pub/Sub构建实时消息系统

6.Session共享存入redis
我们可以把Session存入redis,来进行共享
Session共享:刚开始只有一台服务器,用户访问服务器,登录成功,会产生一个session,下次登录cookie中携带session过来我们就可以确定该用户为合法用户.随着网站的发展,会变成分布式的有多台服务器,假设有三台机器,A访问通过第一台服务器,登录成功以后,A用户的session值只存在在第一台服务器上,第二台和第三台服务器没有,如果A下次访问,不是访问的第一台服务器,就找不到session,会返回请登录页面,就造成刚刚访问过又要登录的情景,所以需要进行session共享.
session共享解决方案:1.文件复制的方式(一般不采用) 2.存到专门存放session的服务器上(存放在session服务器mysql中 后来存放到session服务器的redis中)

memcache简介

介绍下载网址:
memcached.org
https://lupython.gitee.io/2017/05/05/MemCache介绍/

redis和memcache区别:
redis支持数据持久化,落地到硬盘
memcache 不支持数据持久化,断电数据丢失
新浪博客用的memcache 但是他们将新浪博客改为持久化的了 memcacheq

1.memcache内存管理机制

memcache内存管理机制:
内存碎片化?
如果用c语言直接 malloc,free 来向操作系统申请和释放内存时,
在不断的申请和释放过程中,形成了一些很小的内存片断,无法再利用.
这种空闲,但无法利用内存的现象,—称为内存的碎片化.

memcache是将数据存入到内存中进行管理的,故在MemCache中也存在内存碎片化的现象,因此为了解决内存碎片化带来的浪费,
在MemCache中采用的是slab allocator来进行缓解内存的碎片化.

slab allocator的原理:
在启动MemCache时,把内存划分成数个slabclass仓库(每个slabclass大小1M),各仓库,切分成不同尺寸的小块(chunk).
需要存内容时,判断内容的大小,为其选取合理的仓库.

如何将数据存入到MemCache中呢?
memcache根据收到的数据的大小,选择最适合数据大小的chunk组(slabclass)
memcache中保存着slabclass内空闲chunk的列表,根据该列表选择空的chunk, 然后将数据缓存于其中.

注意:
有100byte的内容要存,但122大小的仓库中的chunks满了
并不会寻找更大的,如144的仓库来存储,而是把122仓库的旧数据踢掉!

固定大小chunks带来的内存浪费:
比如当我们将100字节的数据缓存到122字节的chunk中的时候,剩余的22字节就浪费了
这个问题没有根治的方法,只能缓解

2.memcache两种删除机制

1.过期数据惰性删除机制
    ①当某个值过期后,并没有从内存删除
    ②当get值时, 首先判断是否过期,如果过期, 则返回空, 并且清空该值, 此时curr_item就减少了.

    即–这个过期,只是让用户看不到这个数据而已,并没有在过期的瞬间立即从内存删除.这个称为lazyexpiration,惰性失效.好处— 节省了cpu时间和检测的成本

2.LRU删除机制
    以122byte大小的chunk举例,122的chunk都满了, 又有新的值(长度为120)要加入, 要挤掉谁?
   原理 : 当某个单元被请求时,维护一个计数器,通过计数器来判断最近谁最少被使用.就把谁踢出去,即使某个key是设置的永久有效期,也一样会被踢出来!即–永久数据被踢现象

 

原文地址:https://www.cnblogs.com/lizeqian1994/p/10690123.html