文章目录
- 数据结构
- 特殊的数据结构
- bitmap
- 1.string
- 命令
- 1.单值缓存
- 2.对象缓存
- 3.分布式锁
- 4.计数器
- 2.Hash
- 常用命令
- 应用场景
- 应用场景
- 4.Set
- 5.Sorted Set
- zset为什么不用红黑树和用B+树
- 合理的数据编码
- 扩容机制
数据结构
string:最基本的数据类型,二进制安全的字符串,最大512M。
list:按照添加顺序保持顺序的字符串列表,列表(实现队列,元素不唯一,先入先出原则)
set:无序的字符串集合,不存在重复的元素 集合(各不相同的元素)
sorted set:有序集合,已排序的字符串集合。
hash:key-value对的一种集合,hash散列值(hash的key必须是唯一的)
特殊的数据结构
hyperloglogs、geospatial indexes、bitmaps、streams
HyperLogLogs(基数统计)
Geo:Redis3.2 推出,地理位置定位,用于存储地理位置信息,并对存储信息进行操作。
HyperLogLog:用来做基数统计算法✁数据结构,如统计网站UV。
Bitmaps :用一个比特位来映射某个元素状态,在 Redis 中,它底层于字符串类型实现,可以bitmaps 成作一个以比特位为单位数组
bitmap
介绍:bitmap 存储的是连续的二进制数字(0 和 1),通过 bitmap, 只需要一个 bit 位来表 示某个元素对应的值或者状态,key 就是对应元素本身 。我们知道 8 个 bit 可以组成一个 byte,所以 bitmap 本身会极大的节省储存空间。使用场景:适合需要保存状态信息(比如是否签到、是否登录…)并需要进一步对这些信息进 行分析的场景。比如用户签到情况、活跃用户情况、用户行为统计(比如是否点赞过某个视 频)
B.布隆过滤器能判断一定不存在,但是不能判断一定存在。
C.双删不能完全解决一致性问题,所以缓存数据尽量不要参与逻辑处理。
1.string
字符串string:字符串类型是Redis中最为基础的数据存储类型,是一个由字节组成的序列,他在Redis中是二进制安全的,这便意味着该类型可以接受任何格式的数据,如JPEG图像数据货Json对象描述信息等,是标准的key-value,一般来存字符串,整数和浮点数。Value最多可以容纳的数据长度为512MB应用场景:很常见的场景用于统计网站访问数量,当前在线人数等。incr命令(++操作)
可以用来做最简单的数据领存,可以缆存集个简单的字特串,也可以领存某json格式的字符串,Reds分布式锁的实现就为/用了这种教结构,还包括可以实现计教器Session共享、分布式ID
底层实现
String是最简单的数据类型,一般用于复杂的计数功能的缓存:微博数,粉丝数等。
底层实现方式:动态字符串sds 或者 long
(1)什么是sds:
sds全称是Simple Dynamic String,具有如下显著的特点:
① 可动态扩展内存。sds表示的字符串其内容可以修改,也可以追加。
② 采用预分配冗余空间的方式来减少内存的频繁分配,从而优化字符串的增长操作
③ 二进制安全(Binary Safe)。sds能存储任意二进制数据,而不仅仅是可打印字符。
④ 与传统的C语言字符串类型兼容。
redis的String 底层数据结构使用sds
① 性能高:
② 内存预分配,优化字符串的增长操作
③ 惰性空间回收,优化字符串的缩短操作
string类型底层 sds写的
Redis底层是使用C语言实现的,对于字符串类型,其做出了改进,是一种基于动态字符串sds实现,redis作为数据库,查询必然多,修改也会有一定多,sds解决了C语言字符串动态扩展的不方便,以及查询长度操作从O(n)变为了O(1)。 sds相比C语言原始字符串最大优势在于空间预分配,惰性空间释放,性能得到很大提高
C 语言的字符串是 char[]实现的,而 Redis 使用 SDS(simple dynamic string) 封装
Redis 为什么选择 SDS 结构,而 C 语言原生的 char[]不香吗?
举例其中一点,SDS 中,O(1)时间复杂度,就可以获取字符串长度;而 C 字符串,需要遍历整个字符串,时间复杂度为 O(n)
命令
自加:incr
自减:decr
加: incrby
减: decrby
应用场景:共享 session、分布式锁,计数器、限流
/*** 普通缓存放入并设置时间** @param key 键* @param value 值* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期* @return true成功 false 失败*/
public boolean setTimeout(String key, Object value, long time) {try {if (time > 0) {redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);} else {set(key, value);}// log.debug("新增redis里的key。key = [{}],当前时间为:timeDate = [{}],设置过期时间为:time = [{}]秒",GsonUtils.bean2json(key), getNowTimeString(),time);return true;} catch (Exception e) {log.error("根据 key:{}设置缓存数据失败!", key, e);return false;}
}
1.单值缓存
SET key value
GET key
使用上面两条命令可以做用户id存储、商品库存存储等等
2.对象缓存
以缓存user对象为例,有以下两种方式
①:SET user:1 value(json格式数据):把对象转json存入redis,也是当下常用的方式,获取数据需要做数据转换
②:MSET user:1:name zhuge user:1:balance 1888
MGET user:1:name user:1:balance
使用Mset命令,把对象拆开存储,每一个key只保存对象的一个字段信息,适用于经常修改user的某个字段的场景
3.分布式锁
SETNX product:10001 true //操作product:10001
。。。执行业务操作
DEL product:10001 //删除product:10001
其中SETNX key value 命令要求如果key已存在,则其他的setnx命令无法对当前key进行操作。
在使用分布式锁时通常还会通过
SET product:10001 true ex 10 nx 命令设置key的超时时间,防治死锁!
4.计数器
INCR 文章id
可以使用INCR命令实现数量自增,可以用于文章阅读量、热度人数统计等,用户每点进去一次执行一次INCR命令!
5.分布式系统全局序列号
在分布式系统下,如果需要分库分表, mysql的数据库自增id已经无法满足分库分表下的id自增,这时就需要一个独立于数据库之外的中间件来实现id的分配。
redis的INCR命令可以实现id、序列号的生成,但如果用户量非常大,每生成一个id、序列号都去redis会给redis添加不小的压力,我们可以一次性从redis中自增1000次,把序列号放入本地内存中,这1000个id用完了,再去redis再取1000个,可有效降低redis的压力
2.Hash
Hash适合用于存储对象,因为一个对象的各个属性,正好对应一个hash结构的各个field,可以方便地操作对象中的某个字段。
底层数据结构
内部编码:ziplist(压缩列表) 、hashtable(哈希表)
(1)底层实现方式:压缩列表ziplist 或者 字典dict
哈希表:可以用来存储一些keyvalue对,更适合用来存储对象
常用命令
简单使用举例:hset key field value、hget key field
hset:添加hash数据
hget:获取hash数据
hmget:获取多个hash数据
hset person name bingo
hset person age 20
hset person id 1
hget person name
person = {"name": "bingo","age": 20,"id": 1
}
/**
* 放入map里面的数据
* @param mapValue
* @param key
* @param value
*/
public void putMap(String mapValue, String key, String value){
redisTemplate.opsForHash().put(mapValue, key, value);
// log.debug("新增redis里的key。key = [{}],timeDate = [{}]",key, getNowTimeString());
}/*** 获取map里面的所有数据* @param mapValue*/
public Map<String, String> getMap(String mapValue){
Map<Object, Object> data = redisTemplate.opsForHash().entries(mapValue);
if(MapUtils.isNotEmpty(data)){Map<String, String> result = new HashMap<>();data.entrySet().stream().forEach(o -> result.put((String)o.getKey(), (String)o.getValue()));return result;
}
return Collections.emptyMap();
}/*** 获取map里面的指定数据* @param mapValue*/public String getMapValue(String mapValue, String key){Object value = redisTemplate.opsForHash().get(mapValue, key);if(!ObjectUtils.isEmpty(value)){return (String)value;}return null;}/*** 判断map里面的指定数据是否存在* @param mapValue*/public Boolean hasMapValue(String mapValue, String key){return redisTemplate.opsForHash().hasKey(mapValue, key);}/*** 删除map里面的数据* @param mapValue*/public void delMap(String mapValue, String key){redisTemplate.opsForHash().delete(mapValue, key);
// log.debug("删除redis里的key。key = [{}],timeDate = [{}]",key, getNowTimeString());}Map<String,String> user = new HashMap();
user.put("key1","value1");
user.put("key2","value2");
user.put("key3","value3");
//存入
jedis.hmset("user",user);
//取出user中key1
List<String> nameMap = jedis.hmget("user","key1");
//删除其中一个键值
jedis.hdel("user","key2");
//是否存在一个键
jedis.exists("user");
//取出所有的Map中的值:
Iterator<String> iter = jedis.hkeys("user").iterator();
while(iter.next()){jedis.hmget("user",iter.next());
}
应用场景
缓存用户信息
散列hash:Redis中的散列可以看成具有String key和String value的map容器,可以将多个key-value存储到一个key中。每一个Hash可以存储4294967295个键值对。
应用场景:例如存储、读取、修改用户属性(name,age,pwd等)
这个是类似 map 的一种结构,这个一般就是可以将结构化的数据,比如一个对象(前提是这个对象没嵌套其他的对象)给缓存在 redis 里,然后每次读写缓存的时候,可以就操作 hash 里的某个字段。
3.List
list 的实现为一个双向链表,经常被用作队列使用,支持在链表两端进行push和pop操作,时间复杂度为O(1);同时也支持在链表中的任意位置的存取操作,但是需要对list进行遍历,时间复杂度为O(n)。
list 的应用场景非常多,比如微博的关注列表,粉丝列表,消息列表等功能都可以用Redis的 list 结构来实现。可以利用lrange命令,做基于redis的分页功能。
(1)Redis3.2之前的底层实现方式:压缩列表ziplist 或者 双向循环链表linkedlist
(2)Redis3.2及之后的底层实现方式:quicklist
quicklist是一个双向链表,而且是一个基于ziplist的双向链表,quicklist的每个节点都是一个ziplist,结合了双向链表和ziplist的优点
内部编码:ziplist(压缩列表)、linkedlist(链表)
我之前用redis list 做促销活动
秒杀
redis list 加lua 嘎嘎乱杀
应用场景: 消息队列,文章列表
简介:列表(list)类型是用来存储多个有序字符串,一个列表最多可以存储
2^32-1 个元素。
简单实用举例: lpush key value [value …] 、lrange key start end
内部编码:ziplist(压缩列表)、linkedlist(链表)
lpush+lpop=Stack(栈)
lpush+rpop=Queue(队列)
lpsh+ltrim=Capped Collection(有限集合)
lpush+brpop=Message Queue(消息队列)
list常用命令
简单实用举例: lpush key value [value …] 、lrange key start end
lpush:从左边推入
lpop:从右边弹出
rpush:从右变推入
rpop:从右边弹出
llen:查看某个list数据类型的长度
0开始位置,-1结束位置,结束位置为-1时,表示列表的最后一个位置,即查看所有。
lrange mylist 0 -1
比如可以搞个简单的消息队列,从 list 头怼进去,从 list 尾巴那里弄出来。
lpush mylist 1
lpush mylist 2
lpush mylist 3 4 5
rpop mylist
/*** 获取list全量数据* @param key*/
public List<String> listAllData(String key){
List<Object> range = redisTemplate.opsForList().range(key, 0L, -1L);
if(!CollectionUtils.isEmpty(range)){List<String> result = new ArrayList<>();range.stream().forEach(o -> result.add((String)o));return result;
}
return Collections.emptyList();
}
/*** 把数据放入list* @param key* @param value*/
public void putList(String key, String value){redisTemplate.opsForList().rightPush(key, value);// log.debug("新增redis里的key。key = [{}],timeDate = [{}]",key, getNowTimeString());
}
应用场景
消息队列,文章列表
列表list:Redis的列表允许用户从序列的两端推入或者弹出元素,列表由多个字符串值组成的有序可重复的序列,是链表结构。好比Java的linkedList,在往两端插入和删除数据时,效率是非常高的,往中间插入数据效率是很低下的。List中可以包含的最大元素数量是4294967295。
应用场景:1.最新消息排行榜。2.消息队列,以完成多程序之间的消息交换。可以用push操作将任务存在list中(生产者),然后线程在用pop操作将任务取出进行执行。(消费者)
4.集合:和列表类似,也可以存储多个元素,但是不能重复,集合可以进行交售
3.列表:Redis的列表通过命令的组合,既可以当做栈,也可以当做队列来使用,可以用来绩存类似微信公众号、微博等消息流数据并集、差集操作,从而可以实现类似,我和某人共同关注的人、朋友圈点赞等功能
list 是有序列表,这个可以玩儿出很多花样。
比如可以通过 list 存储一些列表型的数据结构,类似粉丝列表、文章的评论列表之类的东西。
比如可以通过 lrange 命令,读取某个闭区间内的元素,可以基于 list 实现分页查询,这个是很棒的一个功能,基于 redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西,性能高,就一页一页走。
4.Set
set是一个存放不重复值的无序集合,可做全局去重的功能,提供了判断某个元素是否在set集合内的功能,这个也是list所不能提供的。基于set可以实现交集、并集、差集的操作,计算共同喜好,全部的喜好,自己独有的喜好等功能。
(1)底层实现方式:有序整数集合intset 或者 字典dict
简介:集合(set)类型也是用来保存多个✁字符串元素,但是不允许重复元素
简单使用举例:sadd key element [element …]、smembers key
内部编码:intset(整数集合)、hashtable(哈希表)
注意点:smembers 和 lrange、hgetall 都属于比较重的命令,如果元素过多存
在阻塞Redis的可能性,可以使用 sscan 来完成。
set 是无序集合,自动去重。
直接基于 set 将系统里需要去重的数据扔进去,自动就给去重了,如果你需要对一些数据进行快速的全局去重,你当然也可以基于 jvm 内存里的 HashSet 进行去重,但是如果你的某个系统部署在多台机器上呢?得基于 redis 进行全局的 set 去重。
可以基于 set 玩儿交集、并集、差集的操作,比如交集吧,可以把两个人的粉丝列表整一个交集,看看俩人的共同好友是谁?对吧。
把两个大 V 的粉丝都放在两个 set 中,对两个 set 做交集。
集合set:Redis的集合是无序不可重复的,和列表一样,在执行插入和删除和判断是否存在某元素时,效率是很高的。集合最大的优势在于可以进行交集并集差集操作。Set可包含的最大元素数量是4294967295。
应用场景:1.利用交集求共同好友。2.利用唯一性,可以统计访问网站的所有独立IP。3.好友推荐的时候根据tag求交集,大于某个threshold(临界值的)就可以推荐。
sadd:添加数据
scard:查看set数据中存在的元素个数
sismember:判断set数据中是否存在某个元素
srem:删除某个set数据中的元素
undefined Redis Set的随机元素选取
Redis Set提供了SRANDMEMBER命令,可以随机地从Set中选取一个元素。该命令有两种用法:
1.1 SRANDMEMBER key:从Set中随机选取一个元素,不会将该元素从Set中移除。
1.2 SRANDMEMBER key count:从Set中随机选取count个不同的元素,并以数组的形式返回。如果参数count为负数,则会从Set中随机选取|count|个元素,但这些元素有可能会重复。
#-------操作一个set-------
#添加元素
sadd mySet 1查看全部元素
smembers mySet判断是否包含某个值
sismember mySet 3删除某个/些元素
srem mySet 1
srem mySet 2 4查看元素个数
scard mySet随机删除一个元素
spop mySet#-------操作多个set-------
#将一个set的元素移动到另外一个set
smove yourSet mySet 2#求两set的交集
sinter yourSet mySet求两set的并集
sunion yourSet mySet
#求在yourSet中而不在mySet中的元素
sdiff yourSet mySet
/**
* 把数据放入list
* @param key
* @param value
*/
public void putSet(String key, String value){
redisTemplate.opsForSet().add(key, value);
// log.debug("新增redis里的key。key = [{}],timeDate = [{}]",key, getNowTimeString());
}/*** 获取set全量数据* @param key*/
public Set<String> getAllSetData(String key){Set<Object> members = redisTemplate.opsForSet().members(key);if(!CollectionUtils.isEmpty(members)){Set<String> result = new HashSet<>();members.stream().forEach(o -> result.add((String)o));return result;}return Collections.emptySet();
}
应用场景:
用户标签,生成随机数抽奖、社交需求。
自带一个随机获得值
spop myset
5.Sorted Set
Sorted set 相比 set 多了一个权重参数score,集合中的元素能够按score进行排列。可以做排行榜应用,取TOP N操作。另外,sorted set可以用来做延时任务。最后一个应用就是可以做范围查找。
(1)底层实现方式:ziplist 或者 skiplist
有序集合(zset)
简介:已排序的字符串集合,同时元素不能重复
简单格式举例:zadd key score member [score member …],zrank key member
底层内部编码:ziplist(压缩列表)、skiplist(跳跃表)
应用场景:排行榜,社交需求(如用户点赞)。
有序集合:集合是无序的,有序集合可以设置顺序,可以用来头现排行棒功能
有序集合zset:和set很像,都是字符串的集合,都不允许重复的成员出现在一个set中。他们之间差别在于有序集合中每一个成员都会有一个分数(score)与之关联,Redis正是通过分数来为集合中的成员进行从小到大的排序。尽管有序集合中的成员必须是卫衣的,但是分数(score)却可以重复。
应用场景:可以用于一个大型在线游戏的积分排行榜,每当玩家的分数发生变化时,可以执行zadd更新玩家分数(score),此后在通过zrange获取几分top ten的用户信息。
常用命令
sort set和hash很相似,也是映射形式的存储
zadd:添加
zcard:查询
zrange:数据排序
sorted set 是排序的 set,去重但可以排序,写进去的时候给一个分数,自动根据分数排序。
zadd board 85 zhangsan
zadd board 72 lisi
zadd board 96 wangwu
zadd board 63 zhaoliu获取排名前三的用户(默认是升序,所以需要 rev 改为降序)
zrevrange board 0 3
获取某用户的排名
zrank board zhaoliu
/**** @param key* @param value*/
public void setOnlineSeat(String key, String value) {redisUtils.putZSet(RedisCacheKeyPrefix.INTERFACE_NUM + key, value, System.currentTimeMillis()/1000 + seatTtl);
}public void removeOnlineSeat(String key, String value) {redisUtils.removeZSet(RedisCacheKeyPrefix.INTERFACE_NUM + key, value);
}
zset为什么不用红黑树和用B+树
zset为什么不用红黑树
插入、删除、查找以及迭代输出有序序列这几个操作红黑树也可以完成,且时间复杂度跟跳表一样。但按照区间来查找数据这个操作红黑树的效率没有跳表高, 跳表可以做到 O(logn) 的时间复杂度定位区间的起点,再在原始链表中顺序往后遍历,非常高效。
其他原因还有跳表更容易代码实现;跳表更加灵活,它可以通过改变索引构建策略,有效平衡执行效率和内存消耗。
Tips: 跳表不能完全替代红黑树,红黑树比跳表的出现要早一些,很多编程语言中的 Map 类型都是通过红黑树来实现,做业务开发时可以直接拿来用,不用费劲自己去实现,但跳表没有现成的实现,所以在开发中如果想使用跳表必须要自己实现。
zset为什么不用B+树
B+树的原理是叶子节点存储数据,非叶子节点存储索引,B+树的每个节点可以存储多个关键字,它将节点大小设置为磁盘页的大小,充分利用了磁盘预读的功能。每次读取磁盘页时就会读取一整个节点,每个叶子节点还有指向前后节点的指针,为的是最大限度的降低磁盘的IO;因为数据在内存中读取耗费的时间是从磁盘的IO读取的百万分之一。而Redis是内存中读取数据,不涉及IO,因此使用了跳表;
合理的数据编码
String:如果存储数字的话,是用 int 类型的编码;如果存储非数字,小于等于39 字节的字符串,是 embstr;大于 39 个字节,则是 raw 编码。
List:如果列表的元素个数小于 512 个,列表每个元素的值都小于 64 字节(默认),使用 ziplist 编码,否则使用 linkedlist 编码
Hash:哈希类型元素个数小于 512 个,所有值小于 64 字节的话,使用ziplist 编码,否则使用 hashtable 编码。
Set:如果集合中的元素都是整数且元素个数小于 512 个,使用 intset 编码,否则使用 hashtable 编码。
Zset:当有序集合的元素个数小于 128 个,每个元素的值小于 64 字节时,使用ziplist 编码,否则使用 skiplist(跳跃表)编码
扩容机制
Redis肯定也有扩容机制,因为如果没有扩容的话,会导致链表越来越长,从而降低查询性能。只不过Redis的扩容机制跟HashMap有点不一样,Redis会有2个hashTable,第二个table只有再扩容的时候使用,当第一个table的容量达到一定量,这个量正常是已有的数据是table大小的时候就会扩容,但是当有在进行持久化的时候,使用量是table容量的5倍的时候扩容
扩容也不会一下子都扩容完成,因为一下子把所有的数据从第一个table移到到第二个table耗时太长。所以会采用渐进式rehash,分批次的将数据迁移到第二个table。然后把第一个table变量指向新table。第二个table赋空。