1.redis的特性:单线程

由于是单线程,所以redis的命令执行是串行而不是并行的,意味着同一时间内redis只会执行一个命令。

由于一次只能执行一条命令,所以要拒绝长命令(就是运行时间长的命令),因为会引起后面的命令阻塞。长命令如:keys,flushall,flushdb,mutil/exec等。

单线程为什么这么快:

因为redis是纯内纯操作。

其实redis不全是单线程,在执行普通读写命令时是单线程,在进行aof持久化时会单独开一个线程进行。

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次
$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 "
";$_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

时间复杂度越高,执行命令消耗的时间越长。