Redis数据类型及应用场景
一、Redis定位与特性
1.SQL与NoSQL
在绝大多数情况,我们会优先考虑使用关系型数据库来存储业务数据,比如SQLServer、Oracle、MySQL等等。
关系型数据库特点
- 它以表格的形式,基于行存储数据,是一个二维模式
- 它存储的是结构化数据,数据存储有固定的模式(schema),数据需要适应表结构
- 表与表之间存在关联关系(Relationship)
- 大部分关系型数据库都支持SQL的操作。支持复杂的关联查询
- 通过支持事务来提供严格或者实时的数据一致性
关系型数据库限制
- 扩容只能垂直扩展,比如磁盘限制了数据的存储,就需要扩大磁盘容量,通过堆硬件的方式,不支持动态的扩缩容。水平扩容需要复杂的技术来实现,比如分库分表
- 表结构修改困难,因此存储的数据库格式也受到限制
- 关系型数据库通常会把数据持久化到磁盘,在高并发和高数据量的情况下,基于磁盘的读写压力比较大
非关系型数据库特点
- 存储非结构化数据,比如文本、图片、音频、视频等等
- 表与表之间没有关联,可扩展性强
- 保证数据的最终一致性,遵循BASE理论。(Basically Available基本可用、Soft-state软状态、EventuallyConsistent最终一致性)
- 支持海量数据的存储和高并发的高效读写
- 支持分布式,能够堆数据进行分片存储,扩缩容简单
2. Redis特性
- 为什么把数据存放在内存中?
- 内存的速度更快
- 减少计算的时间,减轻数据库压力
- 如果是用内存的数据结构作为缓存,为什么不用HashMap或者Memcached?
- 丰富的数据类型
- 支持多种编程语言
- 功能丰富:持久化机制、内存淘汰策略、事务、发布订阅、pipeline、lua
- 支持集群、分布式
二、Redis数据类型及应用
Redis包括5种基本数据类型:String、Hash、List、Set、Zset。以及使用比较少的其他三种数据类型:BitMaps、Hyperloglogs、Geo等
1.String
存储类型
String可以用来存储INT(整数)、float(单精度浮点数)、String(字符串)
操作命令
# 获取指定范围的字符
getrange key1 0 1
# 获取value长度
strlen key1
# 字符串追加
append key1 good
# 设置多个值
mset key1 1 key2 2
# 获取多个值
mget key1 key2
#设置值,如果key存在,则不成功
setnx key1 1
#多参数,实现分布式锁,加过期时间,加nx属性
set key value[expiration EX seconds|PX milliseconds][NX|XX]
set key1 value1 EX 10 NX
# 整数值递增(值不存在得到1)
incr key1
incrby key1 100 # 一次增100
# 整数值递减
decr key2
decr by key2 100
# 浮点数增量
set myfloat 1.1
incrbyfloat myfloat 2.5
应用场景
- 缓存
Stirng类型,缓存热点数据。比如网站首页、微博热搜、报表数据等。可以显著提升热点数据的访问速度 - 分布式数据共享
因为Redis是分布式的独立服务,可以在多个应用之间共享。 - 分布式锁
String类型的setnx方法,只有不存在时才能添加成功,返回true。public Boolean getLock(String key,String value,long timeoutSeconds) { return redisTemplate.opsForValue().setIfAbsent(key, value, timeoutSeconds, TimeUnit.SECONDS); } for (int i = 0; i < 5; i++) { Boolean lock = redisUtils.getLock("lockKey", "lockValue", 3); System.out.println("第" + (i + 1) + "次获取锁,状态:" + lock); Thread.sleep(1000); } //第1次获取锁,状态:true //第2次获取锁,状态:false //第3次获取锁,状态:false //第4次获取锁,状态:true //第5次获取锁,状态:false
- 全局id
INT类型,INCRBY,利用原子性public Object getIdTest() throws Exception { int cpuNum = Runtime.getRuntime().availableProcessors();// 获取处理器数量 int threadNum = cpuNum * 2 + 1;// 根据cpu数量,计算出合理的线程并发数 ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( threadNum - 1, threadNum, Integer.MAX_VALUE, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(Integer.MAX_VALUE), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { super.rejectedExecution(r, e); } } ); long start = System.currentTimeMillis(); int count = 100000; CountDownLatch countDownLatch = new CountDownLatch(count); for (int i = 0; i < count; i++) { threadPoolExecutor.execute(() -> { Long id = redisTemplate.opsForValue().increment("id"); map.merge(id, 1, Integer::sum); countDownLatch.countDown(); }); } countDownLatch.await(); Set<Long> longs = map.keySet(); for (Long aLong : longs) { if (map.get(aLong) > 1) { throw new Exception("出现重复id:" + aLong + ",出现次数:" + map.get(aLong)); } } long end = System.currentTimeMillis(); long result = end - start; return "执行完毕,耗时:" + result + "ms";//执行完毕,耗时:2965ms }
- 计数器
INT类型,INCR方法
例如:文章的阅读量、微博点赞数、允许一定的延迟,先写入Redis再定时同步到数据库 - 限流
INT类型,INCR方法
以访问者的IP和其他信息作为key,访问一次增加一次计数,超过次数则返回false。public Object limitingTest() { Random ra =new Random(); for (int i = 0; i < 10; i++) { int finalI = i; ThreadPoolUtils.execute(()->{ int random = ra.nextInt(10) + 1; for (int j = 1; j <= 10; j++) { Long increment = redisTemplate.opsForValue().increment("user_" + finalI); if (Integer.parseInt(String.valueOf(increment)) >= random) { System.out.println("user_" + finalI + "获取访问限制次数为:" + random + ",已经访问次数为:"+increment+",当前访问次数:" + j + ",访问收到限制!"); } } }); } return ""; }
- 总结:利用redis本身的特性,和String内存的存储内容,以及提供的操作方法,我们可以用来达到很多的业务目的
2.Hash哈希
Hash用来存储多个无序的键值对。最大存储数量2^32-1(40亿左右)
。
注:
- 这个Hash和Redis的存储结构的Hash不是一个东西,那个是外层hash,现在说的是内层hash。
- Hash的value只能是字符串,不能嵌套其他类型,比如hash或者list。
Hash和String的主要区别
- hash把所有相关的值聚集在一个key中,节省内存空间
- 只使用一个key,减少key冲突
- 当需要批量获取值的时候,只需要一个命令,减少内存/IO/CPU的消耗
Hash不适合使用的场景
- Field不能单独设置过期时间,Field的过期时间依赖于key,key过期,整条过期。
- 需要考虑数据量分布的问题(field非常多的时候,无法分布到多个节点)
操作命令
# 单个设置field
hset h1 field1 1
hset h1 field2 2
# 一次设置多个field
hmset h2 field1 1 field2 2 field3 3 field4 4
# 获取某个key中的某个field
hget h1 field1
##批量获取某个key中的多个field值
hmget h1 field1 field2 field3
# 获取key中的所有字段
hkeys h1
# 获取key中的所有字段的值
hvals h1
# 获取key中的字段长度
hlen h1
# 删除key中的某个field,可传入多个
hdel h1 field1 filed2
RedisTemplate中封装的方法
应用场景
存储对象类型的数据,比如一个对象或者一张表的数据,比String节省了更多key的空间,也更加便于集中管理
- 购物车
key:用户id;field:商品id、商品数量
商品数量+1:hincr
商品数量-1:hdecr
删除商品:hdel key field1
全选:hgetall
商品数量:hlen
3.List
存储类型
存储有序的字符串(从左到右),元素可以重复。最大存储数量2^32-1(40亿左右)
。
3.2版本之后,用quicklist(此对象中标明了头部和尾部、以及count元素数量和len节点长度)来存储。quicklist存储了一个双向链表,每个节点quicklistNode
都是一个ziplist,所以是ziplist和linkedlist的结合体。
quicklist是一个数组+链表的结构
操作命令
- 元素增减
# 列表的左侧塞入一个值 (left)头部 lpush myqueue a # 1 a # 列表塞入多个值 (left) lpush myqueue b c d # 4 d c b a # 列表右侧塞入多个值(right)尾部 rpush myqueue h #5 d c b a h # 列表左侧弹出一个值 lpop myqueue # d c b a h # 列表右侧弹出一个值 rpop myqueue # h c b a
- 取值
# 取出索引位置的value lindex myqueue 0 # c lrange myqueue 0 -1 # c b a
RedisTemplate中封装的方法
应用场景
list主要用在存储有序内容的场景。
- 列表
例如用户的消息列表、网站的公告列表、活动列表、博客的文章列表、评论列表等等。思路:存储所有字段,使用lrange queue start end
命令取出一页,按顺序显示。 - 队列/栈
List可以用作分布式环境的队列/栈使用。
List提供了两个阻塞的弹出操作:BLPOP/BRPOP,可以设置超时时间(单位:秒)
BLPOP:移除并获取列表的第一个元素,如果列表没有元素会阻塞队列直到等待超时或发现可弹出元素位置。# 头部弹出 blpop queue1 queue2... timeout #尾部弹出 brpop queue1 queue2... timeout
BRPOP:移除列表的最后一个元素,如果列表没有元素会阻塞队列直到等待超时或发现可弹出元素位置。
4.Set
存储类型
Set存储String类型的无序集合,最大存储数量2^32-1
。Redis用intset或hashtable存储set。如果元素都是整数类型,就用inset存储,如果不是整数类型或元素个数超过512个(可配置),就用hashtable(数组+链表)
操作命令
# 添加一个或者多个元素
sadd myset a b c d e f g # 7
# 获取所有元素
smembers myset # 无序展示
# 统计元素个数
scard myset # 7
# 随机获取一个元素【传入count可以随机获取count个元素】
srandmember myset [count]
# 随机弹出一个元素,从set中删除【传入count可以随机获取count个元素】
spop myset [count]
# 移除一个或者多个元素
srem myset a b c
//查看元素是否存在
sismember myset a
RedisTemplate中封装的方法
应用场景
-
抽奖
随机获取n个(不传为1)元素:srandmember myset n
随机移除n个(不传为1)元素:spop myset n -
点赞、打卡、签到
如一条微博id为wb001
,用户id是u001,u002,u003...
点赞这条微博:sadd wb001 u001 u002…
u001取消点赞:srem wb001 u001
u001是否点赞:sismember wb001 u001
点赞的所有用户:smembers wb001
点赞数:scard wb001 -
商品标签
如用i5001和i5002
来维护两件商品的所有标签
sadd i5001 画面清晰
sadd i5001 流畅sadd i5002 流畅
sadd i5002 真彩显示屏
获取差集:sdiff i5001 i5002
#(画面清晰、真彩显示屏)
获取交集:sinter i5001 i5002
#(流畅)
获取并集
#(画面清晰、流畅、真彩显示屏) -
用户关注、推荐模型
相互关注、我关注的人也关注了他、可能认识的人
5.Zset
存储类型
sorted set 存储有序的元素。每个元素有个score,按照score的从小到大排序。score相同的,按照key的ASCII码排序。
跳表
比如上面组数据,我要查找一个分值为14的数据(流程看红线)
- 首先在最外层找到7,发现比7大,继续找,发现没有了
- 然后就降级,从7找到25,发现比7大,比25小,则在此范围内继续降级
- 找到比7大,比15小,继续降级
- 找到比7大比11大,则往后找,找到比11大,比15小,说明此zset中没有分值为14的数据
数据结构对比
数据结构 | 是否允许重复元素 | 是否有序 | 有序实现方式 |
---|---|---|---|
列表List | 是 | 是 | 索引下标 |
集合Set | 否 | 否 | 无 |
有序集合Zset | 否 | 是 | 分值score |
操作命令
# 添加元素
zadd mzset 10 java 20 python 30 php
# 获取全部元素 正序 [携带分数]
zrange mzset 0 -1 withscores
# 获取全部元素 倒序 [携带分数]
zrevrange mzset 0 -1 withscores
# 根据分值区间获取区间内分值的key
zrangebyscore mzset 10 20 # java python
#移除元素,也可以根据score rank删除
zrem mzset php # 1
# 统计元素个数
zcard mzset # 2
# 分值递增
zincrby mzset 5 java # 15
# 根据分值统计个数
zcount mzset 10 20 # 2
# 获取元素rank(等级,排在第一个的就是0,根据分数依次递增,每次+1)
zrank mzset java # 0
# 获取元素score
zscore mzset java # 15
RedisTemplate中封装的方法
redisTemplate.opsForZSet();
应用场景
顺序会动态变化的列表
- 排行榜
例如百度热搜,微博热榜。
在Zset:hotNews中有一个id为9000的新闻用户每点击一次加1:zincrby hotNews 1 9000
获取今天点击最多的15条新闻:zervrange hotNews 0 15 withscores
三、应用场景总结
场景 | 优点 |
---|---|
缓存 | 提升数据访问速度,减轻数据库压力 |
共享数据 | 分布式环境下的数据存储共享 |
全局ID | 分布式全局ID的生成方案(分库分表) |
分布式锁 | 进程间共享数据的原子操作保证 |
在线用户统计和计数 | zset |
队列、栈 | 跨进程、分布式的队列、栈 |
消息队列 | 异步解耦的消息机制 |
服务注册与发现 | RPC通信机制的服务协调中心(Dubbo) |
购物车 | hash |
新浪/Twitter用户消息时间线列表 | list |
抽奖逻辑(礼物、转发) | Set |
点赞、签到、打卡 | Set |
商品标签 | Set |
用户关注模型、商品推荐模型 | set |
电商产品筛选 | set |
排行榜 | zset |
… | … |