Redis 之五种数据类型简介

Redis 命令参考

数据结构和内部编码

type命令实际返回的就是当前键的数据结构类型。
object encoding命令查询内部编码。
Redis 支持 5 种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合),Zset(SortedSet:有序集合)
Redis的5种数据结构:
在这里插入图片描述
Redis数据结构和内部编码:
在这里插入图片描述
Redis内部设计不同编码的好处:可以随时改进内部编码结构,而对外的数据结构和命令没有影响;Redis会根据值在内部切换适合的内部编码结构,不用的内部编码在不同场景下发挥各自的优势;比如字符串就一个数字,就会使用int,如果是一小段字符串,就使用embstr,如果是一段很长的字符串,就使用raw;例如ziplist比较节省内存,但是在列表元素比较多的情况下,性能就会下降,这时Redis会在内部转化为linkedlist。

全局命令

查看所有键:keys *
键总数:dbsize
dbsize命令在计算键总数时不会遍历所有键,而是直接获取Redis内置的键总数变量,所以dbsize命令的时间复杂度是O(1)。而keys命令会遍历所有键,所以它的时间复杂度是O(n),当Redis保存了大量键时,线上环境禁止使用。
检查键是否存在:exists key(键存在则返回1,不存在则返回0)
删除键:del key [key ...](返回结果为成功删除键的个数,假设删除一个不存在的键,就会返回 0)
设置键过期时间:expire key seconds
查看键剩余过期时间:ttl key(大于等于0的整数:键剩余的过期时间,单位秒、-1:键没设置过期时间、-2:键不存在)
键的数据结构类型:type key(分别是:string、hash、list、set、zset;键不存在返回:none)

单线程架构

Redis使用了单线程架构和I/O多路复用模型来实现高性能的内存数据库服务。

客户端调用Redis服务经历了发送命令、执行命令、返回结果三个过程。
在这里插入图片描述

一条命令从客户端达到服务端不会被立刻执行,所有命令都会进入一个队列中,然后逐个被执行。
在这里插入图片描述
为什么Redis使用单线程模型会达到每秒万级别的处理能力呢?

  • 纯内存访问。
  • 使用I/O多路复用技术的实现。
  • 单线程避免了线程切换和竞态产生的消耗。
  • Redis是C语言开发的,C语言是操作系统语言,执行快。

简单理解IO多路复用: 服务端可以同时hold住多个IO入内存队列,然后每次选出一个sock,然后对这个sock上的事件进行处理。

单线程会有一个问题: 对于每个命令的执行时间是有要求的。如果某个命令执行过长,会造成其他命令的阻塞,对于Redis这种高性能的服务来说是致命的,所以Redis是面向快速执行场景的数据库。

字符串(String)

字符串类型的值实际可以是字符串(简单的字符串、复杂的字符串(例如JSON、XML))、数字 (整数、浮点数),甚至是二进制(图片、音频、视频),但是值最大不能 超过512MB。

设置值:set key value [ex seconds] [px milliseconds] [nx|xx]

  • ex seconds:为键设置秒级过期时间。
  • px milliseconds:为键设置毫秒级过期时间。
  • nx:键必须不存在,才可以设置成功,用于添加。
  • xx:与nx相反,键必须存在,才可以设置成功,用于更新。

setnxsetex的作用和nx、ex选项一样。
setex:设置键并同时设置过期时间,减少一次和Redis服务的网络传输。

注: 因为 SET 命令可以通过参数来实现和 SETNX 、 SETEX 和 PSETEX 三个命令的效果,所以将来的 Redis 版本可能会废弃并最终移除 SETNX 、 SETEX 和 PSETEX 这三个命令。所以建议使用 SET 命令并结合参数使用。

批量操作:mset、mget
在这里插入图片描述
在这里插入图片描述
Redis可以支撑每秒数万的读写操作,但是这指的是Redis服务端的处理能力,对于客户端来说,一次命令除了命令时间还是有网络时间,因为Redis的处理能力已经足够高,对于开发人员来说,网络可能会成为性能的瓶颈。

计数:incr key(默认自增1,键不存在从0开始自增)
自减:decr key
自增指定数字: incrby key increment
自减指定数字: decrby key decrement

追加值:append key value
字符串长度(返回字节数,一个字母或符号占一个字节,一个中文占三个字节):strlen key
设置并返回原值:getset key value

使用场景:

  • 缓存功能
  • 计数
  • 共享Session
  • 限速
    很多应用出于安全的考虑,会在每次进行登录时,让用户输入手机验证码,从而确定是否是用户本人。但是为了短信接口不被频繁访问,会限制用户每分钟获取验证码的频率,例如一分钟不能超过5次。
phoneNum = "138xxxxxxxx"; 
key = "shortMsg:limit:" + phoneNum; 
// SET key value EX 60 NX 
isExists = redis.set(key,1,"EX 60","NX"); 
if(isExists != null || redis.incr(key) <=5)
{ // 通过 }
else
{// 限速 }

内部编码:
Redis会根据当前值的类型和长度决定使用哪种内部编码实现。
字符串类型的内部编码有3种:int(整型)、embstr(小于等于39个字节)、raw(大于39个字节)

哈希(Hash)

在Redis中,哈希类型是指键值本身又是一个键值 结构,形如value={{field1,value1},…{fieldN,valueN}}。
在这里插入图片描述
设置值:hset key field value(设置成功会返回1,反之会返回0)
Redis提供了hsetnx命令,它 们的关系就像set和setnx命令一样,只不过作用域由键变为field。
获取值:hget key field(如果键或field不存在,会返回nil)
删除field:hdel key field [field ...](返回结果为成功删除field的个数)
计算field个数:hlen key

常用命令:hget,hset,hgetall

应用场景:

我们简单举个实例来描述下Hash的应用场景,比如我们要存储一个用户信息对象数据,包含以下信息:
用户ID,为查找的key,
存储的value用户对象包含姓名name,年龄age,生日birthday 等信息,
如果用普通的key/value结构来存储,主要有以下2种存储方式:
第一种方式将用户ID作为查找key,把其他信息封装成一个对象以序列化的方式存储,
如:set u001 “李三,18,20010101”
这种方式的缺点是,增加了序列化/反序列化的开销,并且在需要修改其中一项信息时,需要把整个对象取回,并且修改操作需要对并发进行保护,引入CAS等复杂问题。
第二种方法是这个用户信息对象有多少成员就存成多少个key-value对儿,用用户ID+对应属性的名称作为唯一标识来取得对应属性的值,
如:mset user:001:name "李三 "user:001:age18 user:001:birthday “20010101”
虽然省去了序列化开销和并发问题,但是用户ID为重复存储,如果存在大量这样的数据,内存浪费还是非常可观的。
那么Redis提供的Hash很好的解决了这个问题,Redis的Hash实际是内部存储的Value为一个HashMap,
并提供了直接存取这个Map成员的接口,
如:hmset user:001 name “李三” age 18 birthday “20010101”
也就是说,Key仍然是用户ID,value是一个Map,这个Map的key是成员的属性名,value是属性值,
这样对数据的修改和存取都可以直接通过其内部Map的Key(Redis里称内部Map的key为field), 也就是通过
key(用户ID) + field(属性标签) 操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题。很好的解决了问题。
这里同时需要注意,Redis提供了接口(hgetall)可以直接取到全部的属性数据,但是如果内部Map的成员很多,那么涉及到遍历整个内部Map的操作,由于Redis单线程模型的缘故,这个遍历操作可能会比较耗时,而另其它客户端的请求完全不响应,这点需要格外注意。
实现方式:
上面已经说到Redis Hash对应Value内部实际就是一个HashMap,实际这里会有2种不同实现,这个Hash的成员比较少时Redis为了节省内存会采用类似一维数组的方式来紧凑存储,而不会采用真正的HashMap结构,对应的value redisObject的encoding为zipmap,当成员数量增大时会自动转成真正的HashMap,此时encoding为ht。

内部编码:
哈希类型的内部编码有两种:

  • ziplist(压缩列表)使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比hashtable更加优秀。当有value大于64字节或者field个数超过512,此时ziplist的读写效率会下降,内部编码会变为hashtable。
  • hashtable(哈希表)hashtable的读写时间复杂度为O(1)。

列表(List)

在这里插入图片描述
在这里插入图片描述
使用场景:

  • 消息队列(lpush+brpop)
    - 消息队列
  • lpush+lpop=Stack(栈)
  • lpush+rpop=Queue(队列)
  • lpsh+ltrim=Capped Collection(有限集合)
  • lpush+brpop=Message Queue(消息队列)

内部编码:

  • ziplist
  • linkedlist:某个元素超过64字节或元素个数超过512使用
  • quicklist:结合了ziplist和linkedlist两者的优势

集合(Set)

Redis除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集。

给用户添加标签:

sadd user:1:tags tag1 tag2 tag5 
sadd user:2:tags tag2 tag3 tag5 
... 
sadd user:k:tags tag1 tag2 tag4 
...

给标签添加用户:

sadd tag1:users user:1 user:3 
sadd tag2:users user:1 user:2 user:3 
... 
sadd tagk:users user:1 user:2 
...

计算用户共同感兴趣的标签:

sinter user:1:tags user:2:tags

应用场景通常为以下几种:

  • sadd=Tagging(标签)
  • spop/srandmember=Random item(生成随机数,比如抽奖)
  • sadd+sinter=Social Graph(社交需求)
    在这里插入图片描述
    集合间的运算在元素较多的情况下会比较耗时,所以Redis提供了上面 三个命令(原命令+store)将集合间交集、并集、差集的结果保存在 destination key中。

常用命令: sadd,srem,spop,sdiff ,smembers,sunion。

应用场景:
共同的好友,共同的兴趣爱好,共同关注的人。
集合类型比较典型的使用场景是标签(tag)。例如一个用户可能对娱乐、体育比较感兴趣,另一个用户可能对历史、新闻比较感兴趣,这些兴趣点就是标签。有了这些数据就可以得到喜欢同一个标签的人,以及用户的共同喜好的标签,这些数据对于用户体验以及增强用户黏度比较重要。例如一个电子商务的网站会对不同标签的用户做不同类型的推荐,比如对数码产品比较感兴趣的人,在各个页面或者通过邮件的形式给他们推荐最新的数码产品,通常会为网站带来更多的利益。

Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。
比如在微博应用中,每个人的好友存在一个集合(set)中,这样求两个人的共同好友的操作,可能就只需要用求交集命令即可。
Redis还为集合提供了求交集、并集、差集等操作,可以非常方便的实

内部编码:

  • intset:当集合中的元素都是整数且元素个数小于512
  • hashtable:当元素个数超过512或某个元素不为整数时

有序集合(Sorted Set)

通过得分进行排序,元素不能重复,但是得分可以重复。
有序集合提供了获取指定分数和元素范围查询、计算成员排名等功能,合理的利用有序集合,能帮助我们在实际开发中解决很多问题。
在这里插入图片描述
添加成员:

zadd key score member [score member ...]
zadd user:ranking 251 tom

计算成员个数:
计算某个成员的分数:
计算成员的排名:
删除成员:
增加成员的分数:
返回指定排名范围的成员:
返回指定分数范围的成员:

使用场景:
有序集合比较典型的使用场景就是排行榜系统
例如视频网站需要对用户上传的视频做排行榜,榜单的维度可能是多个方面的:按照时间、按照播放数量、按照获得的赞数。本节使用赞数这个维度,记录每天用户上传视频的排行榜。
主要需要实现以下4个功能。

添加用户赞数:zadd user:ranking:2016_03_15 3 mike
之后再获得一个赞,可以使用zincrby:zincrby user:ranking:2016_03_15 1 mike
取消用户赞数:zrem user:ranking:2016_03_15 mike
展示获取赞数最多的十个用户:zrevrangebyrank user:ranking:2016_03_15 0 9

内部编码:

  • ziplist
  • skiplist(跳跃表)元素个数超过128个或某个元素大于64字节时使用,因为此时ziplist读写效率下降。

列表、集合、有序集合区别

在这里插入图片描述
常用命令: zadd,zrange,zrem,zcard等

使用场景:

以某个条件为权重,比如按顶的次数排序.
ZREVRANGE命令可以用来按照得分来获取前100名的用户,ZRANK可以用来获取用户排名,非常直接而且操作容易。
Redis sorted set的使用场景与set类似,区别是set不是自动有序的,而sorted set可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。
比如:twitter 的public timeline可以以发表时间作为score来存储,这样获取时就是自动按时间排好序的。
比如:全班同学成绩的SortedSets,value可以是同学的学号,而score就可以是其考试得分,这样数据插入集合的,就已经进行了天然的排序。
另外还可以用Sorted Sets来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。
需要精准设定过期时间的应用
比如你可以把上面说到的sorted set的score值设置成过期时间的时间戳,那么就可以简单地通过过期时间排序,定时清除过期数据了,不仅是清除Redis中的过期数据,你完全可以把Redis里这个过期时间当成是对数据库中数据的索引,用Redis来找出哪些数据需要过期删除,然后再精准地从数据库中删除相应的记录。

BitMap

BitMap 概念

  • BitMap 本身不是一种数据结构,实际上就是字符串,但是它可以对字符串的位进行操作。并且单独提供了一套命令。
  • 可以把 BitMap 想象成一个以位为单位的数组,数组中的每个单元只能存0或者1,数组的下标在 BitMap 中叫做偏移量。
  • 单个 BitMap 的最大长度是512MB,即2^32个比特位。
  • BitMap 用一个比特位来映射某个元素的状态,由于一个比特位只能表示 0 和 1 两种状态;所以 BitMap 能映射的状态有限,但是使用比特位的优势是能大量的节省内存空间。

例如字符串A 一个字节,对应的ASCII码是65,对应的二进制就是01000001,BitMap 就是对A的二进制位进行操作。
在这里插入图片描述
在这里插入图片描述

BitMap 相关命令

# 设置值,offset 偏移量从0开始,其中value只能是 0 和 1
setbit key offset value

# 获取值
getbit key offset

# 获取指定范围内值为 1 的个数
# [start]和[end]代表起始和结束字节数,可选性,不指定默认全部
bitcount key start end

# BitMap间的运算
# operations 位移操作符,枚举值
  AND 与运算 &
  OR 或运算 |
  XOR 异或 ^
  NOT 取反 ~
# result 计算的结果,会存储在该key中
# key1 … keyn 参与运算的key,可以有多个,空格分割,not运算只能一个key
# 当 BITOP 处理不同长度的字符串时,较短的那个字符串所缺少的部分会被看作 0。返回值是保存到 destkey 的字符串的长度(以字节byte为单位),和输入 key 中最长的字符串长度相等。
bitop [operations] [result] [key1] [keyn…]

# 计算Bitmaps中第一个值为targetBit的偏移量
bitpos [key] [targetBit]

BitMap 使用场景

1、用户签到
很多网站都提供了签到功能,并且需要展示最近一个月的签到情况,这种情况可以使用 BitMap 来实现。
根据日期 offset = (今天是一年中的第几天) % (今年的天数),key = 年份:用户id。

# 用户ID为1001在2020年的第1天签到了
setbit 2020:1001 1 1
# 用户ID为1001在2020年的第10天签到了
setbit 2020:1001 10 1

# 用户ID为1002在2020年的第10天签到了
setbit 2020:1002 10 1

2、统计活跃用户(用户登陆情况)

使用日期作为 key,然后用户 id 为 offset,如果当日活跃过就设置为1。

假如:
20201009 活跃用户情况是: [1,0,1,1,0]
20201010 活跃用户情况是 :[ 1,1,0,1,0 ]

统计连续两天活跃的用户总数:

bitop and dest1 20201009 20201010
# dest1 中值为1的offset,就是连续两天活跃用户的ID
bitcount dest1

统计20201009 ~ 20201010 活跃过的用户:

bitop or dest2 20201009 20201010 

3、统计用户是否在线
如果需要提供一个查询当前用户是否在线的接口,也可以考虑使用 BitMap 。即节约空间效率又高,只需要一个 key,然后用户 id 为 offset,如果在线就设置为 1,不在线就设置为 0。

设置2020-01-01用户在线情况:

172.25.201.85:6428> setbit login:users:2020-01-01 0 1
(integer) 0
172.25.201.85:6428> setbit login:users:2020-01-01 5 1 
(integer) 0
172.25.201.85:6428> setbit login:users:2020-01-01 11 1 
(integer) 0
172.25.201.85:6428> setbit login:users:2020-01-01 15 1 
(integer) 0
172.25.201.85:6428> setbit login:users:2020-01-01 19 1 
(integer) 0
172.25.201.85:6428> 

userId=0,5,11,15,19的用户对网站进行了访问,数据存储如下。
在这里插入图片描述

172.25.201.85:6428> setbit login:users:2020-01-01 50 1 
(integer) 0

此时有一个userId=50的用户访问了网站,此时数据存储如下。
在这里插入图片描述
设置2020-01-02用户在线情况:

172.25.201.85:6428> setbit login:users:2020-01-02 0 1 
(integer) 0
172.25.201.85:6428> setbit login:users:2020-01-02 5 1 
(integer) 0

利用bitop and命令计算两天都访问网站的用户:

# 将两天在线的用户使用and操作放到新的bitmap中,两天都在线的偏移量在新的bitmap中为1,否则为0
172.25.201.85:6428> bitop and dest1 login:users:2020-01-01 login:users:2020-01-02
(integer) 7
# 计算出两天都在线的用户总共为两个
172.25.201.85:6428> bitcount dest1
(integer) 2

在这里插入图片描述

4、实现布隆过滤器
解决缓存穿透。

统计每日用户的登录数

每一位标识一个用户ID,当某个用户访问我们的网页或执行了某个操作,就在bitmap中把标识此用户的位设置为1。

这里做了一个 使用 set 和 BitMap 存储的对比。
在这里插入图片描述
可以看到,如果活跃用户数量很多,使用 BitMap 明显更有优势,能节省大量的内存。但如果活跃用户数量较少,还是建议使用 set 存储,BitMap 会产生多余的存储开销。

使用经验

  • BitMap 是 sting 类型,最大 512 MB
  • 注意 setbit 时的偏移量较大时,可能有较大耗时
  • 位图不是绝对好,看具体场景

RedisTemplate 操作 Bit

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @Autowired
    private RedisTemplate redisTemplate;

    @GetMapping("/set/{key}/{offset}/{state}")
    public Boolean set(@PathVariable("offset") Integer offset,
                       @PathVariable("key") String key,
                       @PathVariable("state") boolean state) {
        return setBit(key, offset, state);
    }

    @GetMapping("/get/{key}/{offset}")
    public Boolean get(@PathVariable("offset") Integer offset,
                       @PathVariable("key") String key) {
        return getBit(key, offset);
    }

    @GetMapping("/count/{key}")
    public Long count(@PathVariable("key") String key) {
        return bitCount(key);
    }

    @GetMapping("/op/{destKey}/{key1}/{key2}")
    public Long op(@PathVariable("destKey") String destKey,
                   @PathVariable("key1") String key1,
                   @PathVariable("key2") String key2) {
        // RedisStringCommands.BitOperation.AND、RedisStringCommands.BitOperation.OR
        // RedisStringCommands.BitOperation.XOR、RedisStringCommands.BitOperation.NOT
        RedisStringCommands.BitOperation op = RedisStringCommands.BitOperation.AND;
        return bitOp(op, destKey, key1, key2);
    }

    public boolean setBit(String key, int offset, boolean state) {
        return (boolean) redisTemplate.execute((RedisCallback<Boolean>) con -> con.setBit(key.getBytes(), offset, state));
    }

    public boolean getBit(String key, int offset) {
        return (boolean) redisTemplate.execute((RedisCallback<Boolean>) con -> con.getBit(key.getBytes(), offset));
    }

    public Long bitCount(String key) {
        return (Long) redisTemplate.execute((RedisCallback<Long>) con -> con.bitCount(key.getBytes()));
    }

    public Long bitCount(String key, int start, int end) {
        return (Long) redisTemplate.execute((RedisCallback<Long>) con -> con.bitCount(key.getBytes(), start, end));
    }

    public Long bitOp(RedisStringCommands.BitOperation op, String saveKey, String... desKey) {
        byte[][] bytes = new byte[desKey.length][];
        for (int i = 0; i < desKey.length; i++) {
            bytes[i] = desKey[i].getBytes();
        }
        return (Long) redisTemplate.execute((RedisCallback<Long>) con -> con.bitOp(op, saveKey.getBytes(), bytes));
    }

}

Redis 中 BitMap 的使用场景
Bitmap简介
Redis位图Bitmaps详解
Jedis bitmap

遍历键

全量遍历键: keys *
生产环境应该禁止使用 keys * 命令,因为Redis是单线程架构,如果Redis包含了大量的键,执行keys命令很可能会造成Redis阻塞。

渐进式遍历:
Redis从2.8版本后,提供了一个新的命令scan,它能有效的解决keys命令存在的问题。
和keys命令执行时会遍历所有键不同,scan采用渐进式遍历的方式来解决keys命令可能带来的阻塞问题,每次scan命令的时间复杂度是 O(1),但是要真正实现keys的功能,需要执行多次scan。

每次执行scan,可以想象成只扫描一个字典中的一部分键,直到将 字典中的所有键遍历完毕。scan的使用方法如下:scan cursor [match pattern] [count number]
cursor是必需参数,实际上cursor是一个游标,第一次遍历从0开始,每 次scan遍历完都会返回当前游标的值,直到游标值为0,表示遍历结束。

现在要遍历所有的键。第一次执行scan 0,返回结果分为两个部分:第 一个部分的数字XX就是下次scan需要的cursor,第二个部分是10个键,然后使用新的cursor=“XX”,执行scan XX,这次会得到新的cursor,继续执行scan直到得到结果cursor变为0,说明所 有的键已经被遍历过了。

除了scan以外,Redis提供了面向哈希类型、集合类型、有序集合的扫描遍历命令,解决诸如hgetall、smembers、zrange可能产生的阻塞问题,对应的命令分别是hscan、sscan、zscan,它们的用法和scan基本类似。

遍历集合伪代码:
在这里插入图片描述
渐进式遍历可以有效的解决keys命令可能产生的阻塞问题,但是scan并非完美无瑕,如果在scan的过程中如果有键的变化(增加、删除、修改),那么遍历效果可能会碰到如下问题:新增的键可能没有遍历到,遍历出了重复的键等情况,也就是说scan并不能保证完整的遍历出来所有的键,这些是我们在开发时需要考虑的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值