Redis 详解

时间:2019-08-29
本文章向大家介绍Redis 详解,主要包括Redis 详解使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

REmote DIctionary Server(Redis) 是一个由Salvatore Sanfilippo编写的开源的、基于键值对的缓存与存储系统

现在很多互联网应用的服务端都使用到了Redis,到底大家为什么要用Redis呢?Redis有很多特性,比如高性能、高可用、数据类型丰富、易于扩展、可持久化、原子性等等,我觉得其中的“高性能”和“数据类型丰富”是最具决定性的

分析如下
1.性能
关系型数据库的数据存储在硬盘,在高并发环境下I/O较高,并发能力弱,Redis的数据存储在内存,性能远高于关系型数据库。关系型数据库的性能受 硬件、SQL质量、数据量等方面的影响较大,不能一概而论,但一般认为不超过1万次/秒,而Redis的读的速度可达11万次/秒,写的速度可达8万次/ 秒

另一方面,随着数据量的增大,关系型数据库的查询速度会显著降低,而Redis则不会.
2.数据类型丰富
如果只是因为关系型数据库的性能问题,那么其它缓存(比如Memcache)也能减少数据库的查询次数,为什么是Redis?因为Redis支持字符串 (string)、哈希(hash)、列表(list)、无序集合(set)、有序集合(sorted set)多种数据类型,这些数据类型能在实战中能发挥出强大的 作用,在后续的系列文章中我会一一介绍各种数据类型的应用场景

当然,高而可用,易于扩展,可持久化,原子性等特点也是很重要的特性,深入理解助于实战应用

string类型是实战中应用最多的数据类型,Redis的一些特性决定了string类型的应用场景。

  1. Redis的数据是共享的

  如果将用户信息存储在web服务的本地缓存,则每个web服务都会缓存一份,当用户修改昵称时,需要通知其它web服务更新用户缓存。

  如果将用户信息存储在Redis,则只有一份缓存,所有的web访问的都是同一份缓存,当用户修改昵称时,所有web服务都能同时访问到最新的缓存。

  2. Redis是单线程的

  由于Redis的性能瓶颈在于内存读写速度,而不是CPU,设计者将Redis设计成了单线程模式,其所有操作都是原子性的,避免了多线程带来的复杂性

基于以上两点特性,Redis的string类型主要有以下应用场景。

  1. 计数器

  string类型的incr和decr命令的作用是将key中储存的数字值加一/减一,这两个操作具有原子性,总能安全地进行加减操作,因此可以用string类型进行计数,如微博的评论数、点赞数、分享数,抖音作品的收藏数,京东商品的销售量、评价数等。

  2. 分布式锁

  string类型的setnx的作用是“当key不存在时,设值并返回1,当key已经存在时,不设值并返回0”,“判断key是否存在”和“设值”两个操作是原子性地执行的,因此可以用string类型作为分布式锁,返回1表示获得锁,返回0表示没有获得锁。例如,为了保证定时任务的高可用,往往会同时部署多个具备相同定时任务的服务,但是业务上只希望其中的某一台服务执行定时任务,当定时任务的时间点触发时,多个服务同时竞争一个分布式锁,获取到锁的执行定时任务,没获取到的放弃执行定时任务。定时任务执行完时通过del命令删除key即释放锁,如果担心del命令操作失败而导致锁一直未释放,可以通过expire命令给锁设置一个合理的自动过期时间,确保即使del命令失败,锁也能被释放。不过expire命令同样存在失败的可能性,如果你用的是Java语言,建议使用JedisCommands接口提供的String set(String key, String value, String nxxx, String expx, long time)方法,这个方法可以将setnx和expire原子性地执行,具体使用方式如下(相信其它语言的Redis客户端也应当提供了类似的方法)
3. 存储对象

  利用JSON强大的兼容性、可读性和易用性,将对象转换为JSON字符串,再存储在string类型中,是个不错的选择,如用户信息、商品信息等。

  
  string类型的常用命令可参考http://www.runoob.com/redis/redis-strings.html。

hash类型是一个string类型的field和value的映射表,每个 hash 可以存储 232 - 1 键值对(40多亿),hash类型主要有以下应用场景。

  1. 购物车

  以用户id为key,商品id为field,商品数量为value,恰好构成了购物车的3个要素,如下图所示

hash类型的(key, field, value)的结构与对象的(对象id, 属性, 值)的结构相似,也可以用来存储对象。

  在介绍string类型的应用场景时有所介绍,string + json也是存储对象的一种方式,那么存储对象时,到底用string + json还是用hash呢?

  两种存储方式的对比如下表所示。

当对象的某个属性需要频繁修改时,不适合用string+json,因为它不够灵活,每次修改都需要重新将整个对象序列化并赋值,如果使用hash类型,则可以针对某个属性单独修改,没有序列化,也不需要修改整个对象。比如,商品的价格、销量、关注数、评价数等可能经常发生变化的属性,就适合存储在hash类型里。

  当然,不常变化的属性存储在hash类型里也没有问题,比如商品名称、商品描述、上市日期等。但是,当对象的某个属性不是基本类型或字符串时,使用hash类型就必须手动进行复杂序列化,比如,商品的标签是一个标签对象的列表,商品可领取的优惠券是一个优惠券对象的列表(如下图所示)等,即使以coupons(优惠券)作为field,value想存储优惠券对象列表也还是要使用json来序列化,这样的话序列化工作就太繁琐了,不如直接用string + json的方式存储商品信息来的简单

综上,一般对象用string + json存储,对象中某些频繁变化的属性抽出来用hash存储

  

  hash类型的常用命令可参考http://www.runoob.com/redis/redis-hashes.html

list类型是简单的字符串列表,按照插入顺序排序。每个列表最多可以存储 232 - 1 个元素(40多亿) ,list类型主要有以下应用场景。。

  1. 消息队列

  list类型的lpop和rpush(或者反过来,lpush和rpop)能实现队列的功能,故而可以用Redis的list类型实现简单的点对点的消息队列。不过我不推荐在实战中这么使用,因为现在已经有Kafka、NSQ、RabbitMQ等成熟的消息队列了,它们的功能已经很完善了,除非是为了更深入地理解消息队列,不然我觉得没必要去重复造轮子。

  2. 排行榜

  list类型的lrange命令可以分页查看队列中的数据。可将每隔一段时间计算一次的排行榜存储在list类型中,如京东每日的手机销排行、学校每次月考学生的成绩排名、斗鱼年终盛典主播排名等,下图是酷狗音乐“K歌擂台赛”的昨日打擂金曲排行榜,每日计算一次,存储在list类型中,接口访问时,通过page和size分页获取打擂金曲。(打个小广告,酷狗音乐“K歌擂台赛”每天都能产生一批优质翻唱作品,对普通人优质歌声有兴趣的朋友不妨来听听)。

但是,并不是所有的排行榜都能用list类型实现,只有定时计算的排行榜才适合使用list类型存储,与定时计算的排行榜相对应的是实时计算的排行榜,list类型不能支持实时计算的排行榜,之后在介绍有序集合sorted set的应用场景时会详细介绍实时计算的排行榜的实现。

  3. 最新列表

  list类型的lpush命令和lrange命令能实现最新列表的功能,每次通过lpush命令往列表里插入新的元素,然后通过lrange命令读取最新的元素列表,如朋友圈的点赞列表、评论列表。

  但是,并不是所有的最新列表都能用list类型实现,因为对于频繁更新的列表,list类型的分页可能导致列表元素重复或漏掉,举个例子,当前列表里由表头到表尾依次有(E,D,C,B,A)五个元素,每页获取3个元素,用户第一次获取到(E,D,C)三个元素,然后表头新增了一个元素F,列表变成了(F,E,D,C,B,A),此时用户取第二页拿到(C,B,A),元素C重复了。只有不需要分页(比如每次都只取列表的前5个元素)或者更新频率低(比如每天凌晨更新一次)的列表才适合用list类型实现。对于需要分页并且会频繁更新的列表,需用使用有序集合sorted set类型实现。另外,需要通过时间范围查找的最新列表,list类型也实现不了,也需要通过有序集合sorted set类型实现,如以成交时间范围作为条件来查询的订单列表。之后在介绍有序集合sorted set类型的应用场景时会详细介绍sorted set类型如何实现最新列表。

  那么问题来了,对于排行榜和最新列表两种应用场景,list类型能做到的sorted set类型都能做到,list类型做不到的sorted set类型也能做到,那为什么还要使用list类型去实现排行榜或最新列表呢,直接用sorted set类型不是更好吗?原因是sorted set类型占用的内存容量是list类型的数倍之多(之后会在容量章节详细介绍),对于列表数量不多的情况,可以用sorted set类型来实现,比如上文中举例的打擂金曲排行榜,每天全国只有一份,两种数据类型的内存容量差距可以忽略不计,但是如果要实现某首歌曲的翻唱作品地区排行榜,数百万的歌曲,300多个地区,会产生数量庞大的榜单,或者数量更加庞大的朋友圈点赞列表,就需要慎重地考虑容量的问题了。

  list类型的常用命令可参考http://www.runoob.com/redis/redis-lists.html

 set类型是string类型的集合,其特点是集合元素无序且不重复,每个集合最多可以存储 232 - 1 个元素(40多亿),set类型主要有以下应用场景。

  1. 好友/关注/粉丝/感兴趣的人集合

  set类型唯一的特点使得其适合用于存储好友/关注/粉丝/感兴趣的人集合,集合中的元素数量可能很多,每次全部取出来成本不小,set类型提供了一些很实用的命令用于直接操作这些集合,如

    a. sinter命令可以获得A和B两个用户的共同好友

        b. sismember命令可以判断A是否是B的好友

        c. scard命令可以获取好友数量

        c. 关注时,smove命令可以将B从A的粉丝集合转移到A的好友集合

  需要注意的是,如果你用的是Redis Cluster集群,对于sinter、smove这种操作多个key的命令,要求这两个key必须存储在同一个slot(槽位)中,否则会报出 (error) CROSSSLOT Keys in request don't hash to the same slot 错误。Redis Cluster一共有16384个slot,每个key都是通过哈希算法CRC16(key)获取数值哈希,再模16384来定位slot的。要使得两个key处于同一slot,除了两个key一模一样,还有没有别的方法呢?答案是肯定的,Redis提供了一种Hash Tag的功能,在key中使用{}括起key中的一部分,在进行 CRC16(key) mod 16384 的过程中,只会对{}内的字符串计算,例如friend_set:{123456}和fans_set:{123456},分别表示用户123456的好友集合和粉丝集合,在定位slot时,只对{}内的123456进行计算,所以这两个集合肯定是在同一个slot内的,当用户123456关注某个粉丝时,就可以通过smove命令将这个粉丝从用户123456的粉丝集合移动到好友集合。相比于通过srem命令先将这个粉丝从粉丝集合中删除,再通过sadd命令将这个粉丝加到好友集合,smove命令的优势是它是原子性的,不会出现这个粉丝从粉丝集合中被删除,却没有加到好友集合的情况。然而,对于通过sinter获取共同好友而言,Hash Tag则无能为力,例如,要用sinter去获取用户123456和456789两个用户的共同好友,除非我们将key定义为{friend_set}:123456和{friend_set}:456789,否则不能保证两个key会处于同一个slot,但是如果真这样做的话,所有用户的好友集合都会堆积在同一个slot中,数据分布会严重不均匀,不可取,所以,在实战中使用Redis Cluster时,sinter这个命令其实是不适合作用于两个不同用户对应的集合的(同理其它操作多个key的命令)。

  2. 随机展示

  通常,app首页的展示区域有限,但是又不能总是展示固定的内容,一种做法是先确定一批需要展示的内容,再从中随机获取。如下图所示,酷狗音乐K歌擂台赛当日的打擂歌曲共29首,首页随机展示5首;昨日打擂金曲共200首,首页随机展示30首。

set类型适合存放所有需要展示的内容,而srandmember命令则可以从中随机获取几个。

  3. 黑名单/白名单

  经常有业务出于安全性方面的考虑,需要设置用户黑名单、ip黑名单、设备黑名单等,set类型适合存储这些黑名单数据,sismember命令可用于判断用户、ip、设备是否处于黑名单之中。

  

  set类型的常用命令可参考http://www.runoob.com/redis/redis-sets.html

原文地址:https://www.cnblogs.com/qiao298/p/11428541.html