1.redis的特性:单线程
由于是单线程,所以redis的命令执行是串行而不是并行的,意味着同一时间内redis只会执行一个命令。
由于一次只能执行一条命令,所以要拒绝长命令(就是运行时间长的命令),因为会引起后面的命令阻塞。长命令如:keys,flushall,flushdb,mutil/exec等。
单线程为什么这么快:
因为redis是纯内纯操作。
其实redis不全是单线程,在执行普通读写命令时是单线程,在进行aof持久化时会单独开一个线程进行。
redis在接收和处理读写请求的时候虽然使用的是单线程,但redis采用了多路复用技术来处理网络IO(即用户的读写请求),通过其内置的eventloop事件循环机制监听多个读写事件,从而使得读写请求在单线程下也能并发执行。
而后来为了能够处理更高QPS的请求,redis6.0版本之后开始使用多线程接收和处理用户的网络IO请求,每个线程再使用多路复用技术,能够极高的提升网络IO的效率(对于redis而言,CPU和内存IO不是其瓶颈,网络IO才是)。不过在执行读写操作依旧是单线程处理。
2.redis的数据结构
A.字符串类型
字符串的key是字符串,value可以是字符串,数字,二进制,json,但本质上value也还是字符串。
单个value大小不能超过512M,但实际应用中一般不会存超过100K的内容。
字符串类型的使用场景:
缓存
计数器
分布式锁
等等
常用命令:
get/set/del
incr/decr/incrby/decrby
实战场景1:
记录每一个用户的访问次数,或者记录每一个商品的浏览次数。
方案:键名: userid:pageview 或者 pageview:userid 如pageview:5
使用命令:incr
使用理由:每一个用户访问次数或者商品浏览次数的修改是很频繁的,如果使用mysql这种文件系统频繁修改会造成mysql压力,效率也低。
而使用redis的好处有二:使用内存,很快;单线程,所以无竞争,数据不会被改乱
实战场景2:
缓存频繁读取,但是不常修改的信息,如用户信息,视频信息
方案:
业务逻辑上:先从redis读取,有就从redis读取;没有则从mysql读取,并写一份到redis中作为缓存,注意要设置过期时间。
键值设计上:一种是直接将用户一条mysql记录做序列化(serialize或json_encode)作为值,userInfo:userid 作为键名如:userInfo:1
另一种是以 表名:主键名:字段名:id值 作为键,字段值作为值。如 user:id:name:1 = "zbp"
实战场景3:
分布式id生成器
incr id
例如,mysql做了分布式,数据分摊到每一个mysql服务器,在插入数据时,每一个mysql服务器的id要自增但却不能相同。此时可以使用redis的incr来完成。原因是,redis是单线程,意味并发请求生成id时,生成的id不会重复。(单线程无竞争)
set setnx setxx
set 不管key是否存在都设置
setnx key不存在才设置,相当于新增
set key value xx key存在才设置,相当于修改
mget/mset 批量操作
n次get命令花费的时间 = n次网络时间+n次命令时间
一次mget命令获取n个key的时间 = 1次网络时间+n次命令时间
尤其是客户端(php/Python)和redis服务端不在同一主机上,网络时间就会比较长。
所以尽量用mget,但是mget不要获取太多key,否则要传输的数据过大对网络开销和性能都有负担。
实战场景4:
限定某个ip特定时间内的访问次数
使用 incr + setex
//限定某ip在10秒内访问api的次数不能超过1000次
<?php
$r=new Redis();
$r->connect($RedisHost,$RedisPort);
$redis_key = "arts_api|".$_SERVER["REMOTE_ADDR"];
if(!$r->exists($redis_key)){
$r->setex($redis_key,10,"1");
}else{
$r->incr($redis_key);
//判断是否超过规定次数
if($r->get($redis_key)>1000){
die("访问过快");
}
}
?>
实战场景5:分布式session
我们知道,session是以文件的形式保存在服务器中的; 如果你的应用做了负载均衡,将网站的项目放在多个服务器上,当用户在服务器A上进行登陆,session文件会写在A服务器;当用户跳转页面时,请求被分配到B服务器上的时候,就找不到这个session文件,用户就要重新登陆
如果想要多个服务器共享一个session,可以将session存放在redis中,redis可以独立与所有负载均衡服务器,也可以放在其中一台负载均衡服务器上;但是所有应用所在的服务器连接的都是同一个redis服务器。
实现如下:
以PHP为例
设置php.ini 文件中的session.save_handle 和session.save_path
session.save_handler = Redis
session.save_path = "tcp://47.94.203.119:6379" # 大部分情况下,使用的都是远程redis,因为redis要为多个应用服务
如果为redis已经添加了auth权限(requirpass),session.save_path项则应该这样写
session.save_path = "tcp://47.94.203.119:6379?persistent=1&database=10&auth=myredisG506"
使用redis存储session信息
/**
* 将session存储在redis中
*/
session_start();
echo session_id();
echo "<br>";
$_SESSION['age'] = 26;
$_SESSION['name'] = 'xiaobudiu';
$_SESSION['sex'] = 'man';
var_dump($_SESSION);
# 此时session_id依旧存在cookie中。
redis中的key为 PHPREDIS_SESSION:session_id。
当用户跳转页面的时候,php内部会先根据session_id()获取cookie的session_id,再根据session_id获取到redis中的key
再根据key获取value
所以redis的session是通过cookie中的session_id得知 调用$_SESSION['name']是要获取张三的用户名而不是李四的用户名
如果关闭浏览器,cookie会失效,再打开浏览器的时候,session_id就不见了; 这个时候,虽然redis还保存这张三的session。
但是php已经无法获取到这个session。
所以张三再登陆的时候,会重新生成一个session。此时张三的session会有两个,一个是正在使用的,一个是已经失效的。失效的session不会一直放在redis中占用内存,php自动给这个redis的可以设置了过期时间。你也可以给session手动设置过期时间,通过ini_set('session.gc_maxlifetime',$lifetime)。(如果是文件的形式存储的session,php会定时清理失效的session文件,失效的session就是在浏览器cookie中找不到session_id的session)
我们可以封装一个session类,这个session类在原基础上多了可以对session中的某个属性设置过期时间
封装session类
class Session
{
function __construct($lifetime = 3600)
{
//初始化设置session会话存活时间,如果redis中的key存在超过3600秒,会自动执行session_destory(),具体表现为key被删除
ini_set('session.gc_maxlifetime',$lifetime);
}
/**
* 设置当前会话session的key-value
* @param String $name session name
* @param Mixed $data session data
* @param Int $expire 有效时间(秒)
*/
function set($name, $data, $expire = 600) # session中的单独的某个键也可以设置过期时间,很灵活
{
$session_data = array();
$session_data['data'] = $data;
$session_data['expire'] = time()+$expire;
$_SESSION[$name] = $session_data;
}
/**
* 读取当前会话session中的key-value
* @param String $name session name
* @return Mixed
*/
function get($name)
{
if(isset($_SESSION[$name])) {
if($_SESSION[$name]['expire'] > time()) {
return $_SESSION[$name]['data'];
}else{
self::clear($name);
}
}
return false;
}
/**
* 清除当前session会话中的某一key-value
* @param String $name session name
*/
function clear($name)
{
unset($_SESSION[$name]);
}
/**
* 删除当前session_id对应的session文件(清空当前session会话存储,在redis中的表现为删掉一个session的key,在文件形式session中表现为删除一个session文件)
*/
function destroy()
{
session_destroy();
}
}
在一个会话生命周期中,一个redis的key存着这个会话的$_SESSION所有信息包括 $_SESSION['name'],["age"]等
redis存session比文件存session的优势在: 前者可以做分布式session,后者不行;前者是纯内存操作,更快,后者是文件IO操作
我们可以看一下一个key里面的内容
get PHPREDIS_SESSION:6mmndoqm87st2s75ntlsvbp25q
得到
"name|a:2:{s:4:\"data\";s:3:\"zbp\";s:6:\"expire\";i:1584351986;}age|a:2:{s:4:\"data\";i:18;s:6:\"expire\";i:1584351986;}job|a:2:{s:4:\"data\";s:10:\"programmer\";s:6:\"expire\";i:1584351986;}"
是一堆序列化的内容
所以这种方式相比于使用hash结构来存的效率更低
因为这种方式取其中一个字段name就要将整个key获取出来,而且序列化和反序列化也要消耗性能
使用的时候记得要session_start()
题外话:在网站分布多台机器的时候,要做session分布式才可以跨机器获取session; 如果我们不用session,改用纯cookie代替session,将用户信息都存到cookie中,这样无论用户访问到哪台机器都无所谓,反正都可以在浏览器中获取用户信息。
但是这真的是一种很好的解决分布式session的方式吗?
本人有时候也会做做爬虫,知道有些页面必须登陆后才能访问,如果将用户信息存在cookie,爬虫完全可以伪造一份用户的cookie来访问用户的隐私页面。所以使用cookie会带来这样的安全问题。
或者你的cookie是在浏览器可视的,而使用session,只有session_id在浏览器是可视的,用户具体信息在服务端中你是看不到的。
mget/mset 批量操作
n次get命令花费的时间 = n次网络时间+n次命令时间
一次mget命令获取n个key的时间 = 1次网络时间+n次命令时间
尤其是客户端(php/Python)和redis服务端不在同一主机上,网络时间就会比较长。
所以尽量用mget,但是mget不要获取太多key,否则要传输的数据过大对网络开销和性能都有负担。
B.哈希类型
一个哈希相当于一条mysql记录(类似于map结构)
hget/hset/hdel/hgetall
hexists/hlen
hmget/hmset
实战场景1:
记录每一个用户的访问次数
方案:
键名: user:1:info 字段名:pageview
使用命令:hincrby
和单纯使用字符串类型进行记录不同,这里可以将用户访问次数也放到用户信息中作为一个整体,user:1:info中还存储着name,email,age之类的信息
hgetall/hvals/hkeys
PS:慎用hgetall,因为hgetall会获取一个hash key中的所有字段,这是一个长命令,而redis是单线程,会阻塞住后面的命令的执行。
字符串和哈希类型对比:
将一个用户的信息存为redis字符串和哈希
字符串存储方式:
方案1: 键名 user:1:info 值 序列化后的用户对象
方案2: 键名 user:1:字段名 值 字段值
哈希存储方式:
方案3: 键名 user:1:info 值 用户数据
方案1的优点是设计简单,可节省内存(相对于方案2),缺点一是如果要修改用户对象中的某个属性要将整个用户对象从redis中取出来,二是要对数据进行序列化和反序列化也会产生一定CPU开销。
方案2的优点是可以单独更新用户的属性,无需将这个用户所有属性取出。
缺点一是单个用户的数据是分散的不利于管理,二是占用内存,方案1一个用户的数据用一个key就可以保存,方案2一个用户的数据要多个key才可以保存。
方案3的优点:直观,节省空间,可以单独更新hash中的某个属性
缺点:ttl不好控制
C.列表类型
列表本质是一个有序的,元素可重复的队列
增
rpush/lpush
rpush c b a # cba,插入方向<-,即从右往左
lpush c b a # abc,插入方向->,从左往右
linsert # 在一个元素前或后插入元素
删
lpop/rpop #弹出
lrem #删除
ltrim # 修剪列表返回一个子列表,会影响原列表
查
lrange # 按照范围查询列表返回一个子列表
lindex # 按索引取
llen # 列表长度
改
lset # 修改某索引的值为新值
实战场景1:
微博中的时间轴功能(文章按时间排序,还可以做分页)
方案:做一个列表用于存放某个用户的所有微博id,key为 weiboList:user:1,值为微博id。
做一个哈希,里面放微博的内容
该用户新增一个微博就会忘列表中lpop一个微博id,查询的时候使用lrange即可,分页也可以使用lrange。
blpop/brpop # 是lpop和rpop的阻塞版
当列表长度不为空是,lpop和blpop效果一样。
当列表长度为空,lpop会立刻返回nil,而blpop会等待,直到有元素进入列表,blpop就会执行弹出。
它的应用场景就是消息队列。
小结:
用列表实现栈:lpush+lpop = stack
用列表实现队列:lpush+rpop = queue
用列表实现固定集合: lpush+ltrim = capped collection
用列表实现消息队列:lpush+brpop = message queue
D.集合类型
集合的特点是无序性和确定性(不重复)。
增
sadd
删
srem
scard #个数
sismember #是否存在
srandmember # 随机选n个元素
spop # 随机弹出元素,影响原集合
smembers # 返回所有元素,要慎用,不要获取内容较大的集合
实战场景1:
抽奖
使用spop即可,利用的是它的无序性和不重复
实战场景2:
赞,踩,收藏功能等。
方案: 每一个用户做一个收藏的集合,每个收藏的集合存放用户收藏过的文章id或者商品id。
键名: set:userCol:用户id
值: 文章id
如果使用mysql实现,需要建立多对多关系,要建中间表。
实战场景3:
给文章添加标签
方案:
要创建两种集合,以文章id为键名放标签的集合,以标签id为键名放文章的集合。
创建两种集合是因为我们会查询某标签下有什么文章,也会查询某文章下有什么标签
键名: article:1:tags 值:tag的id
键名: tag:1:users 值:user的id
而且这两个集合创建时要放在一个事务中进行。
sdiff/sinter/sunion # 交集并集差集
实战场景4:
共同好友
E.有序集合
有序集合的特点是 有序,无重复值
zadd key score element
zrem
zscore # 获取分数
zincrby # 增加减少分数
zcard # 元素个数
zrange # 按下标范围获取元素,加上withscores会按分数排序
zrangebyscore # 按照分数范围获取元素
zcount # 按分数范围计算元素个数
zremrangebyrank # 删除指定下标范围的元素
zremrangebyscore
实战场景:
排行榜
最后强调一下,要慎用hgetall,原因如下:
当一个hash的字段数很多,存储的内容很多时,处理hgetall请求会花费较长时间;而redis是单线程,同一时间只能处理一个操作,所以后面的操作都要等待hgetall处理完毕才能处理,很影响效率和性能。
还有一种情况:列表或者集合中存了很多哈希的键名。
通过 lrange 0 -1 或者 smembers 这样的命令取出列表或者集合中所有键名再通过hgetall取出大量的hash,而每个hash中又有大量的字段。这种情况下性能会急剧下降,而且占用大量内存,甚至会造成宕机。
下面总结时间复杂度为n的命令:
String类型
MSET、MSETNX、MGET
List类型
LPUSH、RPUSH、LRANGE、LINDEX、LSET、LINSERT
LINDEX、LSET、LINSERT 这三个命令谨慎使用
Hash类型
HDEL、HGETALL、HKEYS/HVALS
HGETALL、HKEYS/HVALS 谨慎使用
Set类型
SADD、SREM、SRANDMEMBER、SPOP、
SMEMBERS、SUNION/SUNIONSTORE、SINTER/SINTERSTORE、SDIFF/SDIFFSTORE
第二行命令谨慎使用
Sorted Set类型
ZADD、ZREM、
ZRANGE/ZREVRANGE、ZRANGEBYSCORE/ZREVRANGEBYSCORE、ZREMRANGEBYRANK/ZREMRANGEBYSCORE
第二行时间复杂度 O(log(N)+M),需要谨慎使用
其他常用命令
DEL、KEYS
KEYS 谨慎使用
基本上,设置多个值或者获取多个值的命令其时间复杂度为n
时间复杂度越高,执行命令消耗的时间越长。