Redis学习笔记(基础)
- 一、Nosql概述
- 1.1、为什么使用Nosql
- 1.2、什么是Nosql
- 1.3、阿里巴巴演进分析
- 1.4、NoSQL的四大分类
- 二、 Redis入门
- 2.1、概述
- 2.2、Windows使用Redis
- 2.3、linux安装
- 2.4、redis-benchmark性能测试
- 2.5、Redis基础知识
- 三、五大数据类型
- 3.1、Redis-Key
- 3.2、String
- 3.3、List
- 3.4、Set
- 3.5、Hash
- 3.6、Zset
- 四、三种特殊数据类型
- 4.1、Geospatial地理位置
- 4.2、Hyperloglog
- 4.3、Bitmap
- 五、事务
- 5.1、Redis中的基本事务
- 5.2、监控
- 六、Jedis
- 七、Springboot的整合
- 7.1、Springboot整合Redis
- 7.2、自定义RedisTemplate
一、Nosql概述
1.1、为什么使用Nosql
1. 单机MySQL的年代
90年代,一个基本的网站访问量一般不会太大,单个数据库完全足够!
那个时候,更多的去使用静态网页 Html~ 服务器根本没有太大的压力!
思考一下,这种情况下 : 整个网站的瓶颈是什么 ?
- 数据量如果太大,一个机器放不下
- 数据的索引(B+ Tree),一个机器内存也放不下
- 访问量(读写混合),一个服务器承受不住
2. Mencached(缓存)+MySQL+垂直拆分(读写分离)
站80%的情况都是在读,每次都要去查询数据库的话就十分的麻烦! 所以说我们希望减轻数据的压力,我们可以使用缓存来保证效率!
3. 分库分表+水平拆分+MySQL集群
4. 如今年代
为什么使用Nosql?
用户的个人信息,社交网络,地理位置。用户自己产生的数据,用户日志等等爆发式增长!
这时候我们就需要使用NoSQL数据库的,Nosgl 可以很好的处理以上的情况!
1.2、什么是Nosql
NoSQL ——> not only SQL(不仅仅是SQL)
泛指非关系型数据库的,随着web2.0互联网的诞生! 传统的关系型数据库很难对付web2.0时代! 尤其是超大规模的高并发的社区!暴露出来很多难以克服的问题,NOSQL在当今大数据环境下发展的十分迅速,Redis是发展最快的,而且是我们当下必须要掌握的一个技术!
很多的数据类型用户的个人信息,社交网络,地理位置。这些数据类型的存储不需要一个固定的格式!不需要多月的操作就可以横向扩展的 ! Map<String,obiect>
使用键值对来控制 !
Nosql特点
- 方便扩展(数据之间没有关系,很好扩展!)
- 大数据量高性能(Redis一秒可以写8万次读取11万,NoSQL的缓存记录级,是一种细粒度的缓存,性能会比较高!)
- 数据类型是多样的(不需要事先设计数据库如果数据量十分大的表,很多人就无法设计了)
- 传统RDBMS和NoSQL
传统的RDBMS
- 结构化组织
- SQL
- 数据和关系都存在单独的表中
- 操作操作,数据定义语言
- 严格的一致性
- 基础的事务
- ...
NoSQL
- 不仅仅是数据
- 没有固定的查询语言
- 键值对存储,列存储,文档存储,图形数据库(社交关系)
- 最终一致性
- CAP定理和BASE(异地多活)初级架构师
- 高性能,高可用,高可扩
- ...
大数据时代的3V:主要是描述问题的
- 海量Volume
- 多样Variety
- 实时Velocity
大数据时代的3高:主要是对程序的要求
- 高并发
- 高可拓
- 高性能
真正在公司中的实践:NOSOL + RDBMS 一起使用才是最强的
1.3、阿里巴巴演进分析
-
商品的基本信息
名称、价格、商家信息;
关系型数据库就可以解决了! MSOL /oracle (淘宝早年就去IOE了!- 王坚:推荐文章;阿里云的这群疯子:40分钟重要!)淘宝内部的 MySQL 不是大家用的 MySQL -
商品的描述、评论(文字比较多)
文档型数据库中,MongoDB -
图片
分布式文件系统 FastDFS- 淘宝自己的TFS
- Gooale 的GFS
- Hadoop HDFS
- 阿里云的 oss
-
商品的关键字 (搜索)
搜索引擎 solr elasticsearch
Iserach: 多隆(多去了解一下这些技术大佬!)
所有牛逼的人都有一段苦逼的岁月!但是你只要像SB一样的去坚持,终将牛逼! -
商品的交易,外部支付接口
- 内存数据库
- Redis Tair、Memache…
-
商品的交易、外部的支付接口
- 三方应用
要知道,一个简单地网页背后的技术一定不是大家所想的那么简单!
大型互联网应用问题:
- 数据类型太多了!
- 数据源繁多,经常重构!
- 数据要改造,大面积改造
1.4、NoSQL的四大分类
KV键值对
- 新浪:Redis
- 美团:Redis+Tair
- 阿里、百度:Redis + memcache
文档型数据库(bson格式 和json一样)
- MongoDB(一般必须要掌握)
- MongoDB 是一个基于分布式文件存储的数据库,C++ 编写,主要用来处理大量的文档!
- MongoDB 是一个介于关系型数据库和非关系型数据中中间的产品!MongoDB 是非关系型数据库中功能最丰富,最像关系型数据库的!
- ConthDB
列存储数据库
- HBase
- 分布式文件系统
图关系数据库
- 他不是存图形,放的是关系,比如 : 朋友圈社交网络,广告推荐!
- Neo4j , InfoGrid ;
分类 | Examples举例 | 典型应用场景 | 数据模型 | 优点 | 缺点 |
---|---|---|---|---|---|
键值( key-value ) | Tokyo Cabinet/Tyrant,Redis, Voldemort ,Oracle BDB | 内容缓存,主要用于处理大量数据的高访问负载,也用于一些日志系统等等。 | Key 指向Value 的键值对,通常用hash table来实现 | 查找速度快 | 数据无结构化,通常只被当作字符串或者二进制数据 |
列存储数据库 | Cassandra,HBase,Riak | 分布式的文件系统 | 以列簇式存储,将同一列数据存在一起 | 查找速度快,可扩展性强,更容易进行分布式扩展 | 功能相对局限 |
文档型数据库 | CouchDB,MongoDb | Web应用( 与Key-Value类似,Value是结构化的,不同的是数据库能够了解Value的内容) | Key-Value对应的键值对,Value为结构化数据 | 数据结构要求不严格,表结构可变,不需要像关系数据库一样需要预先定义表结构 | 查询性能不高,而且缺乏统一的查询语法。 |
图形(Graph)数据库 | Neo4J, InfoGrid, Infinite Graph | 社交网络,推荐系统等。专注于构建关系图谱 | 图结构 | 利用图结构相关算法。比如最短路径寻址,N度关系查找等 | 很多时候需要对整个图做计算才能得出需要的信息,而且这种结构不太好做分布式的集群方鑫。 |
二、 Redis入门
2.1、概述
Reids是什么?
Redis(Remote Dictionary Server),远程字典服务
是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
免费开源!是当下最热门的NoSQL技术之一Q!也被人们成为结构化数据库!
redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
读的速度是110000次/s,写的速度是81000次/s 。
Redis可以干什么?
- 内存存储,持久化,内存中是断电即失,所以说持久化很重要(rdb、aof)
- 效率高,可以用于高速缓存
- 发布订阅系统
- 地图信息分析
- 计时器,计数器(浏览量!)
- …
特性
- 多样的数据类型
- 持久化
- 集群
- 事务
- …
2.2、Windows使用Redis
- 启动Redis
2. 测试使用Redis
2.3、linux安装
查询其他资料
2.4、redis-benchmark性能测试
参数 | 描述 | 默认值 |
---|---|---|
-h | 指定服务器主机名 | 127.0.0.1 |
-p | 指定服务器端口 | 6379 |
-s | 指定服务器 | socket |
-c | 指定并发连接数 | 50 |
-n | 指定请求数 | 10000 |
-d | 以字节的形式指定 SET/GET 值的数据大小 | 2 |
-k | 1=keep alive 0=reconnect | 1 |
-r | SET/GET/INCR 使用随机 key, SADD 使用随机值 | |
-P | 通过管道传输请求 | 1 |
-q | 强制退出 redis。仅显示 query/sec 值 | |
–csv | 以 CSV 格式输出 | |
-l | 生成循环,永久执行测试 | |
-t | 仅运行以逗号分隔的测试命令列表。 | |
-I | Idle 模式。仅打开 N 个 idle 连接并等待。 |
redis启动后进入到bin目录中,执行以下命令进行性能测试:
# 执行测试性能命令
./redis-benchmark -t set,get -n 100000
执行结果如下:
====== SET ======
100000 requests completed in 1.31 seconds
50 parallel clients
3 bytes payload
keep alive: 1
98.64% <= 1 milliseconds
99.85% <= 2 milliseconds
99.99% <= 3 milliseconds
100.00% <= 3 milliseconds
76335.88 requests per second
====== GET ======
100000 requests completed in 1.26 seconds
50 parallel clients
3 bytes payload
keep alive: 1
99.27% <= 1 milliseconds
100.00% <= 1 milliseconds
79365.08 requests per second
TPS、QPS、RT
在描述系统的高并发能力时,经常根据以下三个指标来决定:
- 响应时间RT:响应时间是指系统对请求作出响应的时间。一个系统通常会提供许多功能,而不同功能的业务逻辑也千差万别,因而不同功能的响应时间也不尽相同。在讨论一个系统的响应时间时,通常是指该系统所有功能的平均时间或者所有功能的最大响应时间。
- 吞吐量TPS:吞吐量是指系统在单位时间内处理请求的数量。不同系统的平均响应时间随用户数增加而增长的速度也不大相同,这也是采用吞吐量来度量并发系统的性能的主要原因。一般而言,吞吐量是一个比较通用的指标,两个具有不同用户数和用户使用模式的系统,如果其最大吞吐量基本一致,则可以判断两个系统的处理能力基本一致。
- 查询率QPS:每秒查询率QPS是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准,在互联网中,经常用每秒查询率来衡量服务器的性能。对应fetches/sec,即每秒的响应请求数,也即是最大吞吐能力,查询率通常是针对单机进行压力测试。
2.5、Redis基础知识
Redis默认有16个数据库
默认使用第0个
- 可以使用DBSIZE查看当前数据库的大小
127.0.0.1:6379> DBSIZE
(integer) 0
- 选择使用的数据库
select index
127.0.0.1:6379> select 2 #切换数据库
OK
127.0.0.1:6379[2]>
- 查看数据库所有的key
keys *
127.0.0.1:6379[2]> keys *
1) "name"
- 清空当前数据库
flushdb [ASYNC]
127.0.0.1:6379[2]> flushdb
OK
127.0.0.1:6379[2]>
- 清空所有数据库
flushall [ASYNC]
127.0.0.1:6379> keys *
1) "name"
2) "article:viewCount"
3) "mylist"
4) "myset:__rand_int__"
5) "counter:__rand_int__"
6) "\xac\xed\x00\x05t\x00\x04info"
7) "namme"
8) "\xac\xed\x00\x05t\x00\x03age"
9) "key:__rand_int__"
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379>
Redis是单线程的!
明白Redis是很快的,官方表示,Redis是基于内存操作,CPU不是Redis性能瓶颈,Redis的瓶颈是根据机器的内存和网络带宽,既然可以使用单线程来实现,就使用单线程了!所有就使用了单线程了!
Redis 是C 语言写的,官方提供的数据为 100000+ 的QPS,完全不比同样是使用 key-vale的Memecache差
Redis为什么是单线程的还这么快?
- 误区一:高性能的服务器一定是多线程的?
- 误区二::多线程(CPU上下文会切换!)一定比单线程效率高!
先去CPU>内存>硬盘的速度要有所了解!
核心:redis是将所有的数据全部放在内存中的,所以说使用单线程去操作效率就是最高的,多线程(CPU上下文会切换:耗时的操作!!!),对于内存系统来说,如果没有上下文切换效率就是最高的!多次读写都是在一个CPU上的,在内存情况下,这个就是最佳的方案!
三、五大数据类型
Redis是一个开源(BSD许可),内存存储的数据结构服务器,可用作数据库,高速缓存和消息队列代理。它支持字符串、哈希表、列表、集合、有序集合,位图,hyperloglogs等数据类型。内置复制、Lua脚本、LRU收回、事务以及不同级别磁盘持久化功能,同时通过Redis Sentinel提供高可用,通过Redis Cluster提供自动分区。
3.1、Redis-Key
- 判断是否存在
existes key
127.0.0.1:6379> set name smulll # 设置一个key-value
OK
127.0.0.1:6379> set age 19
OK
127.0.0.1:6379> keys * #查看所有key
1) "name"
2) "age"
127.0.0.1:6379> EXISTS name # 查看该key是否存在
(integer) 1 #存在
- 移除key
move key db
127.0.0.1:6379> move name 1 # 删除某个库中的key
(integer) 1
- 设置过期时间
expire key seconds
查看当前key剩余时间ttl key
127.0.0.1:6379> set name smull
OK
127.0.0.1:6379> expire name 10
(integer) 1
127.0.0.1:6379> ttl name
(integer) 7
127.0.0.1:6379> ttl name
(integer) 3
127.0.0.1:6379> ttl name
(integer) -2
127.0.0.1:6379> get name
(nil)
- 查看key的类型
type key
127.0.0.1:6379> keys *
1) "name"
2) "age"
127.0.0.1:6379> type name
string
127.0.0.1:6379> type age
string
当遇到不会的命令时可以在官网进行查看文档
3.2、String
90% 的 java程序员使用 redis 只会使用一个String类型!
- 在字符串后面追加字符串
append key value
- 追加字符串,如果当前key不存在,就像相当于
set key
- 追加字符串,如果当前key不存在,就像相当于
127.0.0.1:6379> set key1 c1
OK
127.0.0.1:6379> append key1 zhangsan
(integer) 10
127.0.0.1:6379> get key1
"c1zhangsan"
- 获取字符串的长度
strlen key
127.0.0.1:6379> get key1
"c1zhangsan"
127.0.0.1:6379> strlen key1
(integer) 10
- 自增
incr key
自减decr key
带有步长的自增incrby key increment
带有步长的自减decrby key increment
127.0.0.1:6379> set views 0
OK
127.0.0.1:6379> incr views #自增
(integer) 1
127.0.0.1:6379> decr views #自减
(integer) 0
127.0.0.1:6379> decr views
(integer) -1
127.0.0.1:6379> incrby views 10 #带有步长的自增
(integer) 9
127.0.0.1:6379> decrby views 20 #带有步长的自减
(integer) -11
- 获取部分字符串
getrange key start end
127.0.0.1:6379> set myname hello,zhangsan
OK
127.0.0.1:6379> getrange myname 0 3
"hell"
127.0.0.1:6379> getrange myname 0 -1 #倒序查看 -1是最后一个 和get key一样
"hello,zhangsan"
- 替换部分字符串
setrange key offset value
127.0.0.1:6379> set key1 asdasda112
OK
127.0.0.1:6379> setrange key1 1 xxxx # 替换指定位置开始的字符串
(integer) 10
127.0.0.1:6379> get key1
"axxxxda112"
- 设置过期时间
setex key seconds value
如果不存在设置setnx key value
127.0.0.1:6379> setex key3 30 "zhangsan" #设置key3的值 并且设置过期时间
OK
127.0.0.1:6379> ttl key3 # 查看key3的到期时间
(integer) 25
127.0.0.1:6379> setnx key3 zhangsan
(integer) 0
127.0.0.1:6379> setnx key4 xiaowang1 #如果key4不存在则设置
(integer) 1
127.0.0.1:6379> get key3
(nil)
127.0.0.1:6379> get key4
"xiaowang1"
127.0.0.1:6379> setnx key4 lisi #如果key4存在,则失败
(integer) 0
127.0.0.1:6379> get key4
"xiaowang1"
- 批量设置key
mset key value [key value ...]
批量回去key的值mget key [key ...]
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3
OK
127.0.0.1:6379> keys *
1) "k2"
2) "k1"
3) "k3"
127.0.0.1:6379> mget k1 k2 k3
1) "v1"
2) "v2"
3) "v3"
- 如果不存在则创建(批量)
msetnx key value [key value ...]
msetnx
是一个原子性的操作,要么一起成功,要么一起失败
127.0.0.1:6379> msetnx k1 v111 k4 v444
(integer) 0
127.0.0.1:6379> msetnx k4 011 k5 000
(integer) 1
进阶用法类似于Java中的对象
set user:1{name:zhangsan,age:3}
在redis里的巧妙用法:
user:{id}:{filed}
,如此设计在Redis里是完全OK的
127.0.0.1:6379> mset user:1:name zhangsan user:1:age 19
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "zhangsan"
2) "19"
- 先获取再设置
getset key value
127.0.0.1:6379> getset test mysql #获取设置一个空的 如果没有值则返回nil
(nil)
127.0.0.1:6379> get test
"mysql"
127.0.0.1:6379> getset test redis #如果存在值,则获取原来的值再设置
"mysql"
127.0.0.1:6379> get test
"redis"
String类似的使用场景:value除了是我们的字符串还可以是我们的数字!
- 计数器
- 统计多单位的数量。
- 粉丝数
- 对象缓存存储!
3.3、List
在redis里面,我们可以将list变成栈、队列、阻塞队列
多有的list命令都是使用l
开头
- 将值加到列表的头部
lpush key value [value ...]
将值加到列表的尾部rpush key value [value ...]
获取列表中的部分值lrange key start stop
127.0.0.1:6379> lpush list one tow #在列表的头部加入值
(integer) 2
127.0.0.1:6379> lpush list three #在列表的头部加入值
(integer) 3
127.0.0.1:6379> lrange list 0 -1 #获取列表中所有的值
1) "three"
2) "tow"
3) "one"
127.0.0.1:6379> rpush list righr #在列表的尾部加入值
(integer) 4
127.0.0.1:6379> lrange list 0 -1 #获取列表中所有的值
1) "three"
2) "tow"
3) "one"
4) "righr"
- 移除左边第一个数据
lpop
移除右边第一个数据rpop
127.0.0.1:6379> lrange list 0 -1 #获取列表中所有的值
1) "three"
2) "tow"
3) "one"
4) "righr"
127.0.0.1:6379> lpop list
"three"
127.0.0.1:6379> rpop list
"righr"
127.0.0.1:6379> lrange list 0 -1
1) "tow"
2) "one"
- 通过下标获取列表中的某一个值
lindex key index
127.0.0.1:6379> lrange list 0 -1
1) "tow"
2) "one"
127.0.0.1:6379> lindex list 1
"one"
127.0.0.1:6379> lindex list 0
"tow"
- 获取列表的长度
llen key
127.0.0.1:6379> lrange list 0 -1
1) "tow"
2) "one"
127.0.0.1:6379> llen list
(integer) 2
- 移除指定的值
lrem key count value
127.0.0.1:6379> lrange list 0 -1
1) "one"
2) "tow"
3) "one"
127.0.0.1:6379> lrem list 1 one #移除list集合中指定个数的value,精确匹配
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "tow"
2) "one"
127.0.0.1:6379> lpush list one
(integer) 3
127.0.0.1:6379> lrange list 0 -1
1) "one"
2) "tow"
3) "one"
127.0.0.1:6379> lrem list 2 one
(integer) 2
127.0.0.1:6379> lrange list 0 -1
1) "tow"
- 修剪list
ltrim key start stop
127.0.0.1:6379> lrange mylist 0 -1
1) "hello123"
2) "hello12"
3) "hello1"
4) "hello"
127.0.0.1:6379> ltrim mylist 1 2 #通过下标截取指定长度,截取的时候已经改变了list,只剩下截取的元素
OK
127.0.0.1:6379> lrange mylist 0 -1
1) "hello12"
2) "hello1"
- 移除列表中的最后一个元素,并将它移动到新的列表中
rpoplpush source destination
127.0.0.1:6379> lrange mylist 0 -1
1) "hello3"
2) "hello3"
3) "hello12"
4) "hello1"
127.0.0.1:6379> rpoplpush mylist myotherlist #将原来列表的最后一个元素移动到新的列表中
"hello1"
127.0.0.1:6379> lrange mylist 0 -1
1) "hello3"
2) "hello3"
3) "hello12"
127.0.0.1:6379> lrange myotherlist 0 -1
1) "hello1"
- 在指定下标替换值
lset key index value
127.0.0.1:6379> exists list #判断列表是否存在
(integer) 0
127.0.0.1:6379> lset list 0 item #在不存在的列表中替换值会报错
(error) ERR no such key
127.0.0.1:6379> lpush list value1 #新建列表并且加一个值
(integer) 1
127.0.0.1:6379> lset list 0 item #如果存在会更新值
OK
127.0.0.1:6379> lrange list 0 -1
1) "item"
127.0.0.1:6379> lset list 1 item2 #在不存在的下标下替换值会报错
(error) ERR index out of range
- 在指定列表前面或后面插入一个值
linsert key BEFORE|AFTER pivot value
127.0.0.1:6379> rpush mylist hello
(integer) 1
127.0.0.1:6379> rpush mylist world
(integer) 2
127.0.0.1:6379> linsert mylist after world other #在world后面加入一个other
(integer) 3
127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "world"
3) "other"
127.0.0.1:6379> linsert mylist before world wowowow #在world前面加入一个wowowow
(integer) 4
127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "wowowow"
3) "world"
4) "other"
小结
- 他实际上是一个链表,before Node after ,left,right 都可以插入值
- 如果key 不存在,创建新的链表
- 如果key存在,新增内容
- 如果移除了所有值,空链表,也代表不存在!
- 在两边插入或者改动值,效率最高!中间元素,相对来说效率会低一点~
消息排队!消息队列(Lpush Rpop),栈( Lpush Lpop)
3.4、Set
set中的值不能重复
- 在set里面存储值
sadd key member [member ...]
127.0.0.1:6379> sadd myset hello #在指定set中添加一个元素
(integer) 1
127.0.0.1:6379> sadd myset world
(integer) 1
127.0.0.1:6379> sadd myset world #重复添加会报错
(integer) 0
127.0.0.1:6379> sadd myset Smulll
(integer) 1
- 获取set里面的值
smembers myset
127.0.0.1:6379> smembers myset #获取指定set的值
1) "world"
2) "hello"
3) "Smulll"
- 查看set中是否包含某值
sismember key member
127.0.0.1:6379> smembers myset
1) "world"
2) "hello"
3) "Smulll"
127.0.0.1:6379> sismember myset Smulll #判断某一个元素是否在set中
(integer) 1
127.0.0.1:6379> sismember myset zhangsan
(integer) 0
- 查看set中元素的值
scard key
127.0.0.1:6379> smembers myset
1) "world"
2) "hello"
3) "Smulll"
127.0.0.1:6379> scard myset
(integer) 3
- 移除某个元素
srem key member [member ...]
127.0.0.1:6379> srem myset Smulll
(integer) 1
127.0.0.1:6379> scard myset
(integer) 2
127.0.0.1:6379> smembers myset
1) "world"
2) "hello"
- 随机获取set中的某一个值
srandmember key [count]
127.0.0.1:6379> smembers myset
1) "world"
2) "hello"
3) "smulll"
4) "Smulll"
5) "smulll11"
127.0.0.1:6379> srandmember myset # 随机抽出一个元素
"smulll11"
127.0.0.1:6379> srandmember myset
"Smulll"
127.0.0.1:6379> srandmember myset
"hello"
127.0.0.1:6379> srandmember myset 2 # 随机抽选出指定个数的元素
1) "world"
2) "smulll"
- 随机指定key的某个元素
spop key [count]
127.0.0.1:6379> smembers myset
1) "hello"
2) "Smulll"
3) "smulll11"
4) "world"
5) "smulll"
127.0.0.1:6379> spop myset #随机删除myset中的1个元素
"smulll11"
127.0.0.1:6379> smembers myset
1) "hello"
2) "Smulll"
3) "world"
4) "smulll"
127.0.0.1:6379> spop myset 2 #随机删除myset中的2个元素
1) "world"
2) "smulll"
127.0.0.1:6379> smembers myset
1) "hello"
2) "Smulll"
- 将set下指定某个元素移动到另外的set中
smove source destination member
127.0.0.1:6379> smembers myset
1) "world"
2) "hello"
3) "smulll"
4) "smulll1"
5) "smulll2"
127.0.0.1:6379> smove myset myset2 smulll
(integer) 1
127.0.0.1:6379> smembers myset
1) "smulll1"
2) "hello"
3) "smulll2"
4) "world"
127.0.0.1:6379> smembers myset2
1) "smulll"
- 获取几个set中的差集
sdiff key [key ...]
获取几个set中的交集sinter key [key ...]
获取几个set中的并集sunion key [key ...]
127.0.0.1:6379> smembers set1
1) "c"
2) "b"
3) "a"
127.0.0.1:6379> smembers set2
1) "c"
2) "e"
3) "d"
4) "a"
127.0.0.1:6379> sdiff set1 set2 #获取 set1 和 set2 的差集
1) "b"
127.0.0.1:6379> sinter set1 set2 #获取 set1 和 set2 的交集
1) "c"
2) "a"
127.0.0.1:6379> sunion set1 set2 #获取 set1 和 set2 的并集
1) "e"
2) "a"
3) "b"
4) "c"
5) "d"
使用场景:
- 微博,A用户将所有关注的人放在一个set集合中!将它的粉丝也放在一个集合中!
- 共同关注,共同爱好,二度好友,推荐好友!(六度分割理论)
3.5、Hash
Map集合,key-map!时候这个值是一个map集合!本质和string类型没有太大区别,还是一个简单的key-value
set myhash field kuangshen
- 创建一个Hash
hset key field value
127.0.0.1:6379> hset myhash field1 smulll
(integer) 1
- 获取Hash中的一个字段
hget key field
127.0.0.1:6379> hget myhash field1
"smulll"
- 批量添加数据到hash中
hmset key field value [field value ...]
127.0.0.1:6379> hmset myhash field1 smulll fiedl2 zhangsan fiedl3 lisi
OK
- 批量获取hash中的字段值
hmget key field [field ...]
127.0.0.1:6379> hmget myhash field1 fiedl2 fiedl3
1) "smulll"
2) "zhangsan"
3) "lisi"
- 获取全部字段值
hgetall key
127.0.0.1:6379> hgetall myhash
1) "field1"
2) "smulll"
3) "fiedl2"
4) "zhangsan"
5) "fiedl3"
6) "lisi"
- 删除某一个字段的值
hdel key field [field ...]
127.0.0.1:6379> hdel myhash field1 #删除某个指定的字段
(integer) 1
127.0.0.1:6379> hgetall myhash
1) "fiedl2"
2) "zhangsan"
3) "fiedl3"
4) "lisi"
- 查看hash中的字段数量
hlen key
127.0.0.1:6379> hset myhash f1 zhangsan
(integer) 1
127.0.0.1:6379> hset myhash f1 zhangsan f2 lsii f3 wangwu
(integer) 2
127.0.0.1:6379> hgetall myhash #获取全部的hash字段
1) "f1"
2) "zhangsan"
3) "f2"
4) "lsii"
5) "f3"
6) "wangwu"
127.0.0.1:6379> hlen myhash #获取字段长度
(integer) 3
- 按段hash的某个字段是否存在
hexists key field
127.0.0.1:6379> hgetall myhash #获取全部的hash字段
1) "f1"
2) "zhangsan"
3) "f2"
4) "lsii"
5) "f3"
6) "wangwu"
127.0.0.1:6379> HEXISTS myhash f1
(integer) 1
- 只获取hash所有的字段
hkeys key
只获取hash所有的值hvals key
127.0.0.1:6379> hkeys myhash
1) "f1"
2) "f2"
3) "f3"
127.0.0.1:6379> hvals myhash
1) "zhangsan"
2) "lsii"
3) "wangwu"
- 给某个字段自增
HINCRBY key field increment
127.0.0.1:6379> hset myhash fi1 1
(integer) 1
127.0.0.1:6379> HINCRBY myhash fi1 1
(integer) 2
127.0.0.1:6379> HINCRBY myhash fi1 -9
(integer) -7
- 判断是否存在以及是否能设置
在这里插入代码片
127.0.0.1:6379> hsetnx myhash field1 zhangsan #不存在则设置
(integer) 1
127.0.0.1:6379> hsetnx myhash field1 lisi # 存在则不能设置
(integer) 0
使用场景:
- hash变更的数据 user name age,尤其是是用户信息之类的,经常变动的信息!
- hash更适合于对象的存储
- String更适合字符串的存储
127.0.0.1:6379> hset user:1 name zhangsan
(integer) 1
127.0.0.1:6379> hget user:1 name
"zhangsan"
127.0.0.1:6379> hset user:1 age 19
(integer) 1
127.0.0.1:6379> hmget user:1 name age
1) "zhangsan"
2) "19"
127.0.0.1:6379> hgetall user:1
1) "name"
2) "zhangsan"
3) "age"
4) "19"
3.6、Zset
在set的基础上,增加了一个值,set k1 v1
| zset k1 score1 v1
- 在Zset中添加值
zadd key [NX|XX] [CH] [INCR] score member [score member ...]
127.0.0.1:6379> zadd myset 1 one #在zset中加一个值
(integer) 1
127.0.0.1:6379> zadd myset 2 two 3 three #在zset中加多个值
(integer) 2
127.0.0.1:6379> zrange myset 0 -1
1) "one"
2) "two"
3) "three"
- 根据score进行排列
zrangebyscore key min max [WITHSCORES] [LIMIT offset count]
(最小值到最大值)
127.0.0.1:6379> zadd salary 1000 zhangsan # 添加数据
(integer) 1
127.0.0.1:6379> zadd salary 2000 lisi
(integer) 1
127.0.0.1:6379> zadd salary 4000 wangwu
(integer) 1
127.0.0.1:6379> zrangebyscore salary -inf +inf #从负无穷到正无穷进行排列
1) "zhangsan"
2) "lisi"
3) "wangwu"
127.0.0.1:6379> zrangebyscore salary -inf +inf withscores #从负无穷到正无穷进行排列并且打印score
1) "zhangsan"
2) "1000"
3) "lisi"
4) "2000"
5) "wangwu"
6) "4000"
127.0.0.1:6379> zrangebyscore salary -inf 2000 withscores #从负无穷到2000进行排列并且打印score
1) "zhangsan"
2) "1000"
3) "lisi"
4) "2000"
- 据score进行排列
zrevrange key start stop [WITHSCORES]
(从最大值到最小值)
127.0.0.1:6379> zrevrange salary 0 -1 withscores
1) "wangwu"
2) "4000"
3) "lisi"
4) "2000"
- 移除zset中的指定元素
在这里插入代码片
127.0.0.1:6379> zrangebyscore salary -inf +inf withscores #从负无穷到正无穷进行排列并且打印score
1) "zhangsan"
2) "1000"
3) "lisi"
4) "2000"
5) "wangwu"
6) "4000"
127.0.0.1:6379> zrem salary zhangsan #移除zhangsan
(integer) 1
127.0.0.1:6379> zrangebyscore salary -inf +inf withscores
1) "lisi"
2) "2000"
3) "wangwu"
4) "4000"
- 获取集合中的个数
zcard key
127.0.0.1:6379> zrangebyscore salary -inf +inf withscores
1) "lisi"
2) "2000"
3) "wangwu"
4) "4000"
127.0.0.1:6379> zcard salary
(integer) 2
- 获取指定区间的成员数量
zcount key min max
127.0.0.1:6379> zadd newset 1 smulll
(integer) 1
127.0.0.1:6379> zadd newset 2 xiaohong 3 xiaohuang 4 xiaowang
(integer) 3
127.0.0.1:6379> zcount newset 1 2 #获取指定区间的成员数量
(integer) 2
其与的一些API,通过我们的学习吗,你们剩下的如果工作中有需要,这个时候你可以去査査看官方文档!
使用场景:
- 案例思路:set 排序 存储班级成绩表,工资表排序!
- 普通消息,1,重要消息 2,带权重进行判断!
- 排行榜应用实现,取Top N 测试!
四、三种特殊数据类型
4.1、Geospatial地理位置
朋友的定位,附近的人,打车距离计算?
Redis 的 Geo 在Redis3.2 版本就推出了!这个功能可以推算地理位置的信息,两地之间的距离,方圆几里的人!
- 添加位置
geoadd key longitude latitude member [longitude latitude member ...]
127.0.0.1:6379> geoadd china:city 116.40 39.90 beijing
(integer) 1
127.0.0.1:6379> geoadd china:city 121.48 31.40 shanghai
(integer) 1
127.0.0.1:6379> geoadd china:city 39.90 116.40 beijing
(error) ERR invalid longitude,latitude pair 39.900000,116.400000
有效的经度从-180度到180度。
有效的纬度从-85.05112878度到85.05112878度。
当坐标位置超出上述指定范围时,该命令将会返回一个错误。
南北极无法添加,我们一般会下载城市数据,直接通过Java一次性导入数据
- 查询位置
在这里插入代码片
127.0.0.1:6379> geopos china:city beijing #获取指定城市的经纬度
1) 1) "116.39999896287918091"
2) "39.90000009167092543"
127.0.0.1:6379> geopos china:city beijing shanghai
1) 1) "116.39999896287918091"
2) "39.90000009167092543"
2) 1) "121.48000091314315796"
2) "31.40000025319353938"
- 获取两个位置之间的距离
geodist key member1 member2 [unit]
127.0.0.1:6379> geodist china:city beijing shanghai #查看背景到上海的直线距离
"1050524.9458"
127.0.0.1:6379> geodist china:city beijing shanghai km #查看背景到上海的直线距离 km为单位
"1050.5249"
m 表示单位为米。
km 表示单位为千米。
mi 表示单位为英里。
ft 表示单位为英尺。
如果用户没有显式地指定单位参数, 那么 GEODIST 默认使用米作为单位。
- GEODIST 命令在计算距离时会假设地球为完美的球形, 在极限情况下, 这一假设最大会造成 0.5% 的误差。
我附近的人?(获得所有附近的人的地址,定位!)通过半径来查询!
- 获取半径内集合中的位置,中心点自己设置
georadius key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC
127.0.0.1:6379> georadius china:city 110 30 10000 km
1) "shanghai"
2) "beijing"
127.0.0.1:6379> georadius china:city 110 30 10000 km withcoord withdist #符带经度纬度
1) 1) "shanghai"
2) "1108.3830"
3) 1) "121.48000091314315796"
2) "31.40000025319353938"
2) 1) "beijing"
2) "1245.2858"
3) 1) "116.39999896287918091"
2) "39.90000009167092543"
127.0.0.1:6379> georadius china:city 110 30 10000 km withcoord withdist count 1 #加上count可以限制查出的数量
1) 1) "shanghai"
2) "1108.3830"
3) 1) "121.48000091314315796"
2) "31.40000025319353938"
- 找出位于指定范围内的元素,中心点是由给定的位置元素决定
georadiusbymember key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DES
127.0.0.1:6379> georadiusbymember china:city beijing 1000 km
1) "beijing"
127.0.0.1:6379> georadiusbymember china:city beijing 3000 km
1) "shanghai"
2) "beijing"
- 返回一个或多个位置元素的 Geohash 表示
在这里插入代码片
该命令将返回11个字符的Geohash字符串,所以没有精度Geohash,损失相比,使用内部52位表示。
127.0.0.1:6379> geohash china:city beijing shanghai
1) "wx4fbxxfke0"
2) "wtw6sk5n300"
GEO 底层的实现原理其实就是 Zset!我们可以使用Zset命令来操作geo!
可以使用zrem
来删除goe位置
127.0.0.1:6379> zrange china:city 0 -1 #查看全部元素
1) "shanghai"
2) "beijing"
127.0.0.1:6379> zrem china:city beijing #删除北京位置
(integer) 1
127.0.0.1:6379> zrange china:city 0 -1
1) "shanghai"
4.2、Hyperloglog
什么是基数?
A{1,3,4,5,7,8,7}
B{1,3,5,7,8}
基数(不重复的元素)= 5,可以接受误差!
简介
Redis 2.8.9 版本就更新了 Hyperloglog 数据结构!
Redis Hyperloglog 基数统计的算法!
- 优点:占用的内存是固定,2^64 不同的元素的基数,只需要废 12KB内存!如果要从内存角度来比较的话 Hyperloglog首选!
- 网页的 UV (一个人访问一个网站多次,但是还是算作一个人!)
传统的方式 ,set 保存用户的id,然后就可以统计 set 中的元素数量作为标准判断!
这个方式如果保存大量的用户id,就会比较麻烦!我们的目的是为了计数,而不是保存用户id;
0.81% 错误率!统计UV任务,可以忽略不计的!
- 添加元素
在这里插入代码片
127.0.0.1:6379> pfadd myset a b j o w s a x r t
(integer) 1
127.0.0.1:6379> pfadd myset2 j r t z d l o p j k
(integer) 1
- 统计元素基数
pfcount key [key ...]
127.0.0.1:6379> pfadd myset a b j o w s a x r t
(integer) 1
127.0.0.1:6379> pfcount myset
(integer) 9
- 合并两个set
pfmerge destkey sourcekey [sourcekey ...]
127.0.0.1:6379> pfmerge myset myset2
OK
127.0.0.1:6379> pfcount myset
(integer) 14
如果允许容错,那么一定可以使用 Hyperloglog!
如果不允许容错,就使用set 或者自己的数据类型即可!
4.3、Bitmap
位图
统计用户信息,活跃,不活跃! 登录 、未登录!打卡,365打卡!两个状态的,都可以使用 Bitmaps!
Bitmaps 位图,数据结构!都是操作二进制位来进行记录,就只有0和1两个状态
365天=365 bit 1字节=8bit 46 个字节左右!
- 设置bitset
setbit key offset value
模拟周一至周日打卡
127.0.0.1:6379> setbit sign 0 1
(integer) 0
127.0.0.1:6379> setbit sign 1 0
(integer) 0
127.0.0.1:6379> setbit sign 2 1
(integer) 0
127.0.0.1:6379> setbit sign 3 1
(integer) 0
127.0.0.1:6379> setbit sign 4 0
(integer) 0
127.0.0.1:6379> setbit sign 5 0
(integer) 0
127.0.0.1:6379> setbit sign 6 1
(integer) 0
- 获取bitset
getbit key offset
127.0.0.1:6379> getbit sign 1
(integer) 0
127.0.0.1:6379> getbit sign 2
(integer) 1
- 统计
bitcount key [start end]
127.0.0.1:6379> bitcount sign
(integer) 4
五、事务
5.1、Redis中的基本事务
Redis 事务本质:一组命令的集合! 一个事务中的所有命令都会被序列化,在事务执行过程的中,会按照顺序执行!
一次性、顺序性、排他性!执行一系列的命令
队列 set set set 执行
Redis事务没有隔离级别的概念!
所有的命令在事务中,并没有直接被执行!只有发起执行命令的时候才会执行!Exec
Redis单条命令式保存原子性的,但是事务不保证原子性!
redis的事务:
- 开启事务
multi -
- 命令入队
......
- 执行事务
exec -
- 取消事务
discard -
执行事务
127.0.0.1:6379> MULTI # 开启事务
OK
# 命令入队
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> exec # 执行事务
1) OK
2) OK
3) "v2"
放弃事务
127.0.0.1:6379> multi #开始事务
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> discard #取消事务
OK
127.0.0.1:6379> get k1 #事务队列中命令都不会执行
(nil)
编译型异常(代码有问题!命令有错!),事务中所有的命令都不会被执行!
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> getset k3 #错误的命令
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> set k5 v5
QUEUED
127.0.0.1:6379> exec #执行事务
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k1 #所有命令都不会执行
(nil)
运行时异常(1/0),如果事务队列中存在语法性,那么执行命令的时候,其他命令是可以正常执行的。错误命令会抛出异常
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr k1 #会执行的时候失败
QUEUED
127.0.0.1:6379> set ke v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> get k3
QUEUED
127.0.0.1:6379> exec
1) (error) ERR value is not an integer or out of range #异常的命令不会执行,不影响其他命令
2) OK
3) OK
4) "v3"
5.2、监控
悲观锁:
- 很悲观,认为什么时候都会出问题,无论做什么都会加锁!
乐观锁:
- 很乐观,认为什么时候都不会出问题,所以不会上锁!更新数据的时候去判断一下,在此期间是否有人修改过这个数据
- 获取version
- 更新的时候比较 version
Redis监视测试
正常执行成功
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> watch money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby money 20
QUEUED
127.0.0.1:6379> incrby out 20
QUEUED
127.0.0.1:6379> exec
1) (integer) 80
2) (integer) 20
测试多线程,使用watch可以当作redis的乐观锁操作
127.0.0.1:6379> watch money #监视 money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby money 20
QUEUED
127.0.0.1:6379> incrby out 20
QUEUED
127.0.0.1:6379> exec #执行之前,另外一个线程修改了值,就会导致事务执行失败
(nil)
如果获取失败,获取最新的值就好
127.0.0.1:6379> unwatch # 解锁
OK
127.0.0.1:6379> watch money # 重新上锁
OK
127.0.0.1:6379> multi #开启事务
OK
127.0.0.1:6379> decrby money 20
QUEUED
127.0.0.1:6379> incrby money 20
QUEUED
127.0.0.1:6379> exec # 结束事务
1) (integer) 60
2) (integer) 80
六、Jedis
我们要使用Java在操作Redis
什么是Jedis 是 Redis 官方推荐的java连接开发工具!使用ava 操作Redis 中间件!如果要使用Java操作redis,那么一定要对jedis十分的熟悉!
- 导入依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>5.1.0</version>
</dependency>
<!--json格式-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.67_noneautotype2</version>
</dependency>
- 测试连接
package com.example;
import redis.clients.jedis.Jedis;
public class redistest {
public static void main(String[] args) {
//创建一个对象
Jedis jedis = new Jedis("127.0.0.1",6379);
//测试连接 jedis所有的命令就是我们之前学习的所有指令
System.out.println(jedis.ping());
}
}
事务测试
package com.example;
import com.alibaba.fastjson.JSONObject;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
public class Redistest2 {
public static void main(String[] args) {
Jedis jedis = new Jedis();
JSONObject jsonObject = new JSONObject();
jedis.flushDB();//清空数据
jsonObject.put("name","zhangsan");
jsonObject.put("age",18);
Transaction multi = jedis.multi();
String string = jsonObject.toString();
try {
multi.set("user1",string);
multi.set("user2",string);
multi.exec();//执行事务
}catch (Exception exception){
multi.discard();//放弃事务
exception.printStackTrace();
}finally {
System.out.println(jedis.get("user1"));
System.out.println(jedis.get("user2"));
jedis.close();
}
}
}
七、Springboot的整合
7.1、Springboot整合Redis
SpringBoot 操作数据:spring-data jpa jdbc mongodb redis!
SpringData 也是和 SpringBoot 齐名的项目!
说明:在 SpringBoot2.x之后,原来使用的jedis 被替换为了 lettuce?
jedis:采用的直连,多个线程操作的话,是不安全的,如果想要避免不安全的,使用jedis pool连接池! BIO
lettuce:采用netty,实例可以再多个线程中进行共享,不存在线程不安全的情况!可以减少线程数据了,更像Nio模式
整合测试
- 导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 配置连接
spring.data.redis.host=127.0.0.1
spring.data.redis.port=6379
- 测试
package com.smulll;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisTemplate;
@SpringBootTest
class RedisTestApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
//opsForValue操作String的
redisTemplate.opsForValue();
//opsForList操作list的
redisTemplate.opsForList();
//opsForSet操作Set的
redisTemplate.opsForSet();
//opsForHash操作Hash的
redisTemplate.opsForHash();
//opsForZSet操作Zset的
redisTemplate.opsForZSet();
//opsForGeo操作Geo的
redisTemplate.opsForGeo();
//opsForHyperLogLog操作HyperLogLog的
redisTemplate.opsForHyperLogLog();
//除了进本的操作,我们常用的方法都可以直接通过redisTemplate操作,比如事务,和基本的CRUD
RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
// connection.flushDb();
// connection.flushAll();
redisTemplate.opsForValue().set("name","Smulll");
System.out.println(redisTemplate.opsForValue().get("name"));
}
}
默认的序列化
7.2、自定义RedisTemplate
一个固定的模板
package com.smulll.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
//这是一个固定模板
@Bean
@SuppressWarnings("all")
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
//我们为了自己开发方便,一般直接使用<String,Object>
RedisTemplate<String,Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
//JSON序列化配置
Jackson2JsonRedisSerializer<Object> objectJackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
objectJackson2JsonRedisSerializer.setObjectMapper(objectMapper);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
//key采用stinger的序列化方式
template.setKeySerializer(stringRedisSerializer);
//hash的key也采用Sring的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
//Value序列化方式采用Jackson
template.setValueSerializer(objectJackson2JsonRedisSerializer);
//hash的序列化方式采用jackson
template.setHashValueSerializer(objectJackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
所有的redis操作,其实对于iava开发人员来说,十分的简单,更重要是要去理解redis的思想和每一种数据结构的用处和作用场景