目录
学习参考: 【狂神说Java】Redis最新超详细版教程通俗易懂
NOSQL概述
为什么要用NoSql呢?可以从数据库的发展说起。(参考:NoSql(一)入门概述,数据库发展史)
第一个阶段:单机MySQL的年代
那个时候网站访问量不大,单个数据库完全可以应付,动态交互不多。
此时架构下,数据库的瓶颈是:
- 数据量如果太大,一个机器会放不下
- 数据库的索引(B+tree)一个机器内存放不下
- 访问量变量(读写混合)一个服务器不能承受
第二个阶段:Memcached (缓存)+ mysql +垂直拆分
在80%的情况下,网站都是在读取数据,如果每一次都需要去查询数据,那么无疑是对数据库压力很大的,并且也不高效。比如一些用户查询的东西是同一个,那么我们给这种数据做成缓存,下一个用户就可以先访问缓存里的数据了。
缓存主要是解决了读的问题
当然这是个发展过程,并不是一开始都是加缓存的,一般都是优化数据结构和索引--->文件缓存(IO)--->Memcached
第三个阶段:MySQL 主从,读写分离
通过缓存解决了读取数据的问题,读写都集中在一个数据库中仍然让数据库不堪重负,大部分网站开始使用主从复制技术来达到读写分离,以提高数据库的可拓展性。
MySQL的主从分离模式成为这个时候网站的标配。
第四个阶段 分库分表+水平拆分+MySQL集群
数据库的操作无非是读和写这两个操作,读取的效率通过缓存可以得到提高,那么写的操作效率该如何提高呢?
MySQL有两个引擎,MylSAM和Innodb。
MylSAM:使用的是表锁,当进行数据查询时,会锁定整个表,直到数据查询出来,而其他的查询进程会因为表的锁定而等待,十分影响效率,在高并发下会出现严重问题。
INNODB:它是行锁,只锁定一行
行锁和表锁都是为了解决写的问题,但切换数据库引擎是物理层面上的操作,所以慢慢的开始使用分库分表解决写的压力。
MySQL在那个年代推出了表分区的功能,但是使用的并不多。后来MySQL推出集群,很好的满足了这个年代的需求。
当今这个阶段
2010-2020 技术已近进入了爆炸式的发展阶段 MySQL等关系型数据库就不够用了,数据量很大,而且变化很快。
MySQL 有的人来使用它存储一些比较大的文件,博客,图片等,数据库很大,效率就很低。如果有一种数据库来处理这种数据,那么MySQL的压力就会变得很小。
于是我们开始研究如何处理这个问题,在大数据IO的压力下,表几乎没法更改。数据库的设计变得很复杂,面面俱到变得很难。
目前一个互联网一个基本架构模型:
所以为什么要用NoSQL呢?
用户的个人信息,社交网络,地理位置,用户自己产生的数据,用户日志的爆发式增长,这些都是关系型数据库无法解决的了。
这个时候我们就需要使用NoSQL数据库了,NoSQL可以很好的处理以上的情况。
关于水平拆分和垂直拆分 : mysql水平拆分与垂直拆分
那么到底什么是NoSQL呢?
NoSQL(NoSQL = Not Only SQL ),意即"不仅仅是SQL"。
在现代的计算系统上每天网络上都会产生庞大的数据量,这些数据有很大一部分是由关系数据库管理系统(RDBMS)来处理。
今天我们可以通过第三方平台(如:Google,Facebook等)可以很容易的访问和抓取数据。用户的个人信息,社交网络,地理位置,用户生成的数据和用户操作日志已经成倍的增加。我们如果要对这些用户数据进行挖掘,那SQL数据库已经不适合这些应用了, NoSQL 数据库的发展却能很好的处理这些大的数据。
RDBMS VS NoSQL
RDBMS
- 结构化组织
- 结构化查询语言
- 数据和关系都存在单独的表中
- 数据操纵语言,数据定义语言
- 严格的一致性
- 基础的事务
NoSQL
- 代表着不仅仅是SQL
- 没有声明性查询语言
- 没有预定义模式
- 键-值对存储,列存储,文档存储,图形数据库(社交关系)
- 最终一致性,而非ACID属性
- 非结构化和不可预知的数据
- CAP定理
- 高性能,高可用性和可伸缩性
NoSQL的特点
- 高可拓展性
- 分布式计算
- 低成本
- 架构的灵活性,半结构化数据
- 没有复杂的关系
NoSQL四大分类
- key-value存储
- 代表:Redis,MemcacheDB
- 可用通过key快速查询到其value,一般来说,存储不管value的格式,照单全收。(Redis包含了其他功能)
- 文档存储
- 代表:MongoDB,CouchDB
- 文档一般用类似于Json的格式存储(用Bson),存储的内容是文档性的。这样也有机会对某些字段建立索引,实现关系数据库的某些功能
- 列存储数据库
- 代表:Hbase、Hypertable、Cassandra
- 顾名思义,是按照列存数据的。最大的特点是方便存储结构化和半结构化数据。方便做数据压缩,对针对某一列或者某激烈的查询有非常大的IO优势。
- 图形关系数据库(不是放图片的,而是放关系的)
- 代表:Neo4J
- 图形关系的最佳存储。使用传统关系数据库来解决的话,性能低下,而且设计使用不方法。
Redis入门
Redis简介
REmote DIctionary Server(Redis) 是完全开源免费的,遵守BSD协议,是一个高性能的key-value数据库。
Redis 与其他 key - value 缓存产品有以下三个特点:
- Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
- Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
- Redis支持数据的备份,即master-slave模式的数据备份。
是一个开源的使用ANSI C语言编写、支持网络、可基于内存也可持久化的日志型,key-Value数据库,并提供多语言API。
免费和开源,是当下最热门的NoSQL技术之一,也被人称为结构化数据库。
Redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
Redis能干嘛呢?
- Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。(一般来说内存中是断电即失,所以说持久化很重要,rdb、aof)
- 效率高,基于用于高速缓存
- 发布订阅系统
- 地图信息分析
- 计时器,计数器(浏览量)
- ..........
Redis的优势
- 多样的数据类型
- 持久化
- 集群
- 事务
Redis 安装
Windows在GitHub上停更很久了,因为Redis推荐的都是在Lniux服务器上搭建的。
Windows安装Redis
下载地址: Redis.3.2.1 64位
解压安装到指定目录即可
解压完成后,双击开启redis服务(redis-server.exe)
理论上双击打开redis服务就可以了,但我遇到了闪退的问题。百度查询后说是在redis安装目录打开cmd命令手动开启redis.server
输入一下命令行就能启动成功了
redis-server.exe redis.windows.conf
启动成功的页面:
使用客户端(redis-cil.exe )连接服务器,输入命令:
ping
显示PONG,就能表示连接成功了。
Windows的安装十分简单,但是Redis还是建议在Lniux下部署。
Lniux安装Redis
如果没有虚拟机,那么你需要有一个云服务器。(虽然云服务器买了以后就闲置了,但好像可以拿来搭建Redis)
Lniux下安装Redis的步骤
- 下载安装包
- 将安装包放进服务器
- 放在/opt文件夹下
- 进行解压
tar -zxvf redis-5.0.8.tar.gz
- 进入解压后的文件,可以看到Redis的配置文件
- 基本环境安装(执行完这三个命令后会自动安装redis)
# 安装gcc
yum install gcc-c++
# make 命令
make
# 安装
make install
- redis 默认安装路径 /usr/local/bin
- 将redis文件复制到当前目录下
# 创建一个文件夹
[root@clawsServer bin]# mkdir cconfig
# 将配置文件复制到cconfig文件目录下
[root@clawsServer bin]# cp /opt/redis-5.0.8/redis.conf cconfig
- redis默认不是后台启动的,需要修改配置文件
- 启动Redis-server 指定配置文件启动
- 启动redis-cli
# 启动redis-cil
[root@clawsServer bin]# redis-cli
# 测试redis服务是否连通
127.0.0.1:6379> ping
# 得到PONG 则表明连接成功
PONG
Redis命令
redis默认有16个数据库,默认使用的是第0个,可以使用select进行切换。
这一点可以在Redis的Conf文件中得到证实。
Redis 键(key)
Redis 键命令用于管理 redis 的键。
Redis keys 命令
1 | 该命令用于在 key 存在时删除 key。 |
2 | 序列化给定 key ,并返回被序列化的值。 |
3 | 检查给定 key 是否存在。 |
4 | EXPIRE key seconds 为给定 key 设置过期时间,以秒计。 |
5 | EXPIREAT 的作用和 EXPIRE 类似,都用于为 key 设置过期时间。 不同在于 EXPIREAT 命令接受的时间参数是 UNIX 时间戳(unix timestamp)。 |
6 | 设置 key 的过期时间以毫秒计。 |
7 | PEXPIREAT key milliseconds-timestamp 设置 key 过期时间的时间戳(unix timestamp) 以毫秒计 |
8 | 查找所有符合给定模式( pattern)的 key |
9 | 将当前数据库的 key 移动到给定的数据库 db 当中。 |
10 | 移除 key 的过期时间,key 将持久保持。 |
11 | 以毫秒为单位返回 key 的剩余的过期时间。 |
12 | 以秒为单位,返回给定 key 的剩余生存时间(TTL, time to live)。 |
13 | 从当前数据库中随机返回一个 key 。 |
14 | 修改 key 的名称 |
15 | 仅当 newkey 不存在时,将 key 改名为 newkey |
16 | SCAN cursor [MATCH pattern] [COUNT count] 迭代数据库中的数据库键。 |
17 | 返回 key 所储存的值的类型。 |
Redis为什么单线程还这么快?
我们明白Redis是很快的,Redis是基于内存操作,CPU不是Redis性能瓶颈,Redis的瓶颈是根据及其的内存和网络带宽,既然可以使用单线程来实现,就使用单线程了。
为什么Redis为什么单线程还这么快?
Redis是将所有的数据都放在内存中的,所以说使用单线程去操作效率就是最高的,因为多线程操作会让CPU上下文切换,这是一个耗时的操作。对内存系统来说,如果没有上下文切换效率就是最高的,多次读写都是在一个CPU上的,在内存情况下,这个就是最佳方案。
Redis五大数据类型
String
Redis 字符串数据类型的相关命令用于管理 redis 字符串值
# 设置指定的key值
SET key value
# 得到指定的key值
GET key
# 返回 key 中字符串值的子字符
GETRANGE key 0 -1
# 如果key已经存在并且是一个字符串,APPEND命令将指定的value追加到该key原来的值(value)的末尾
# 如果key不存在 那么和set功能一样 将添加一个key-value
APPEND key value
#替换key 从x下标的值
SETRANGE key 0
##################################################################################
# setex (set with expire) 设置过期时间
# setnx (set if not exist) 不存在这个值的时候再设置 在分布式锁中会常常使用
127.0.0.1:6379> keys *
1) "key3"
2) "views"
3) "key1"
127.0.0.1:6379> setex key4 10 'hello' # 设置key4的值为hello 并在10秒后过期
OK
127.0.0.1:6379> keys * # 查看所有的key 没有key5这个key
1) "key3"
2) "key4"
3) "views"
4) "key1"
127.0.0.1:6379> setnx key5 'hello' # 设置key5的值为 hello
(integer) 1 # 返回1 设置成功
127.0.0.1:6379> keys * # 查看所有的key key5 存在
1) "key5"
2) "key3"
3) "key4"
4) "views"
5) "key1"
127.0.0.1:6379> setnx key5 'hello' # 再设置一次key5
(integer) 0 # 因为已存在 返回0 设置失败
###############################################################################
# mset,mget 同时设置多个值,同时得到多个值
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 # 同时设置k1 k2 k3的值 分别为v1,v2,v3
OK
127.0.0.1:6379> mget k1 k2 k3 # 同时得到k1 k2 k3的值
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> keys * # 现在k1存在
1) "k2"
2) "k1"
3) "k3"
127.0.0.1:6379> msetnx k1 v1 k4 v4 # 使用msetnx 插入k1(存在) 和 k4(不存在)
(integer) 0 # 插入失败 因为msetnx命令是一个原子性操作,要么一起成功,要么一起失败
##################################################################################
# 先get再set
127.0.0.1:6379> getset db redis
#如果不存在值 则返回nil
(nil)
# 再get db
127.0.0.1:6379> get db
#得到刚刚set的redis的值
"redis"
# 如果值存在,获取到原来的值,并设置新的值
127.0.0.1:6379> getset db mongodb
"redis"
# 再下一次get db的时候 得到新设置的值
127.0.0.1:6379> get db
"mongodb"
设置对象
# 设置一个user:1 对象 值为为json字符串
set user:1 {name:zhangsan,age:3}
# 这里的key是巧妙的设计
# user:{id}:{field} 是可以这样的使用的
127.0.0.1:6379> mset user:1:name zhangsan user:1:age 2
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "zhangsan"
2) "2"
List
Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)
一个列表最多可以包含 232 - 1 个元素 (4294967295, 每个列表超过40亿个元素)。
###############################################################################
# LPUSH key value1 [value2] 将一个或多个值插入到列表头部
127.0.0.1:6379> LPUSH list one
(integer) 1
127.0.0.1:6379> LPUSH list two
(integer) 2
127.0.0.1:6379> LPUSH list three
###############################################################################
# LRANGE key start stop 获取列表指定范围内的元素
127.0.0.1:6379> LRANGE list 0 -1
1) "three"
2) "two"
3) "one"
###############################################################################
# RPUSH 在列表中添加一个或多个值
127.0.0.1:6379> RPUSH list four
(integer) 4
127.0.0.1:6379> LRANGE list 0 -1
1) "three"
2) "two"
3) "one"
4) "four"
###############################################################################
# LPOP key 移出并获取列表的第一个元素
127.0.0.1:6379> LRANGE list 0 -1
1) "three"
2) "two"
3) "one"
4) "four"
# LPOP key 移出并获取列表的第一个元素
127.0.0.1:6379> LPOP list
"three"
###############################################################################
# RPOP 移除列表的最后一个元素,返回值为移除的元素。
127.0.0.1:6379> RPOP list
"four"
127.0.0.1:6379> LRANGE list 0 -1
1) "two"
2) "one"
# LINDEX key index 通过索引获取列表中的元素
127.0.0.1:6379> LINDEX list 0
"two"
127.0.0.1:6379> LINDEX list 1
"one"
################################################################################
# Llen 获取列表长度
127.0.0.1:6379> LRANGE list 0 -1
1) "three"
2) "two"
3) "one"
# Llen 获取列表长度
127.0.0.1:6379> Llen list
(integer) 3
################################################################################
# LREM key count value 移除列表元素
127.0.0.1:6379> Lpush list three
(integer) 4
# LREM key count value 移除列表元素
127.0.0.1:6379> lrem list 1 one
(integer) 1
127.0.0.1:6379> LRANGE list 0 -1
1) "three"
2) "three"
3) "two"
127.0.0.1:6379> lrem list 2 three
(integer) 2
127.0.0.1:6379> LRANGE list 0 -1
1) "two"
127.0.0.1:6379>
################################################################################
# LTRIM key start stop 对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。
127.0.0.1:6379> LPUSH list one
(integer) 1
127.0.0.1:6379> LPUSH list two
(integer) 2
127.0.0.1:6379> LPUSH list three
(integer) 3
127.0.0.1:6379> LPUSH list four
(integer) 4
127.0.0.1:6379> LRANGE list 0 -1
1) "four"
2) "three"
3) "two"
4) "one"
# LTRIM key start stop 对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。
127.0.0.1:6379> ltrim list 0 1
OK
127.0.0.1:6379> LRANGE list 0 -1
1) "four"
2) "three"
###############################################################################
# RPOPLPUSH source destination 移除列表的最后一个元素,并将该元素添加到另一个列表并返回
# 在mylist列表中添加了三个元素
127.0.0.1:6379> rpush mylist 'hello'
(integer) 1
127.0.0.1:6379> rpush mylist 'hello1'
(integer) 2
127.0.0.1:6379> rpush mylist 'hello2'
(integer) 3
# RPOPLPUSH source destination 移除列表的最后一个元素,并将该元素添加到另一个列表并返回
127.0.0.1:6379> rpoplpush mylist myotherlist
"hello2"
#此时查看mylist 只有两个元素了 那么另外一个会在myotherlist里,因为我们把它移过去了
127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "hello1"
127.0.0.1:6379> lrange myotherlist 0 -1
1) "hello2"
127.0.0.1:6379>
###############################################################################
# LSET key index value 将列表中指定下标的值替换为另一个值,更新操作
#判断list列表是否存在
127.0.0.1:6379> exists list
# 返回0 不存在
(integer) 0
# 使用LSET命令 使list的0下标的值替换为 item
127.0.0.1:6379> lset list 0 item
# 因为列表不存在所以报错了
(error) ERR no such key
# 使用lpush命令添加一个list列表
127.0.0.1:6379> lpush list value1
(integer) 1
127.0.0.1:6379> LRANGE list 0 -1
1) "value1"
# 使用LSET命令 使list的0下标的值替换为 item
127.0.0.1:6379> lset list 0 item
OK
# 结果已被替换了 item
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 将某个具体的value插入到列表中某个元素的前面或者后面
#在列表中添加hello 和world 这两个值
127.0.0.1:6379> rpush list hello
(integer) 1
127.0.0.1:6379> rpush list world
(integer) 2
# 查看列表里的值
127.0.0.1:6379> lrange list 0 -1
1) "hello"
2) "world"
# 在world前加other
127.0.0.1:6379> linsert list before world other
(integer) 3
# 查看列表里的值
127.0.0.1:6379> lrange list 0 -1
1) "hello"
2) "other"
3) "world"
# 在other后加strange
127.0.0.1:6379> linsert list after other strange
(integer) 4
# 查看列表里的值
127.0.0.1:6379> lrange list 0 -1
1) "hello"
2) "other"
3) "strange"
4) "world"
127.0.0.1:6379>
总结:
- Redis 的List实际上是一个链表,既可以在前面插入值,也可以在后面插入值
- 如果key不存在,创建新的链表
- 如果key存在,新增内容
- 如果移除了所有的值,空链表,也代表不存在。
- 在两边插入或者改动值,效率最高,插入到中间元素,效率相对来说较低
Set
Redis 的 Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)。
#####################################################################################
#SADD key member1 [member2] 向集合添加一个或多个成员
127.0.0.1:6379> sadd myset one
(integer) 1
127.0.0.1:6379> sadd myset two
(integer) 1
127.0.0.1:6379> sadd myset three
(integer) 1
# SMEMBERS key 返回集合中的所有成员
127.0.0.1:6379> smembers myset
1) "three"
2) "one"
3) "two"
127.0.0.1:6379> sismember myset one
(integer) 1
127.0.0.1:6379> sismember myset four
(integer) 0
# SCARD key 获取集合的成员数
127.0.0.1:6379> scard myset
(integer) 3
# SREM key member1 [member2] 移除集合中一个或多个成员
127.0.0.1:6379> srem myset one
(integer) 1
127.0.0.1:6379> smembers myset
1) "three"
# SRANDMEMBER key [count] 随机抽选出指定个数的元素
127.0.0.1:6379> srandmember myset
"three"
127.0.0.1:6379> srandmember myset
"three"
127.0.0.1:6379> srandmember myset
"two"
# 随机抽取出两个元素
127.0.0.1:6379> srandmember myset 2
1) "three"
2) "two"
#######################################################################
#SPOP key 随机的删除某个元素
127.0.0.1:6379> smembers myset
1) "three"
2) "five"
3) "four"
4) "two"
127.0.0.1:6379> spop myset
"three"
127.0.0.1:6379> spop myset
"two"
#######################################################################
#将一个指定的值移动到另外一个set集合中
127.0.0.1:6379> sadd set1 hello
(integer) 1
127.0.0.1:6379> sadd set1 world
(integer) 1
127.0.0.1:6379> sadd set2 byebye
(integer) 1
127.0.0.1:6379> sadd set2 myworld
(integer) 1
# SMOVE source destination member 将 member 元素从 source 集合移动到 destination 集合
127.0.0.1:6379> smove set1 set2 hello
(integer) 1
127.0.0.1:6379> smembers set2
1) "hello"
2) "myworld"
3) "byebye"
#######################################################################
# 微博,豆瓣这类的网站都有共同关注的功能,那么这个是取交集来实现的。Redis也可以取交集。
# 在set1添加元素
127.0.0.1:6379> sadd set1 a
(integer) 1
127.0.0.1:6379> sadd set1 b
(integer) 1
127.0.0.1:6379> sadd set1 c
(integer) 1
# 在set2添加元素
127.0.0.1:6379> sadd set2 a
(integer) 1
127.0.0.1:6379> sadd set2 b
(integer) 1
127.0.0.1:6379> sadd set2 e
(integer) 1
# SDIFF key1 [key2] 取差集
127.0.0.1:6379> sdiff set1 set2
1) "c"
# SINTER key1 [key2] 取交集
127.0.0.1:6379> sinter set1 set2
1) "a"
2) "b"
# SINTERSTORE destination key1 [key2] 取并集
127.0.0.1:6379> sunion set1 set2
1) "a"
2) "c"
3) "b"
4) "e"
Hash
Redis hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。(想象成为一个Map集合,key-map的形式)
Redis 中每个 hash 可以存储 232 - 1 键值对(40多亿)。
Hash更适合对象的存储
# hset set一个具体的key-value
127.0.0.1:6379> hset myhash name claw
(integer) 1
# hget 获取一个字段的值
127.0.0.1:6379> hget myhash name
"claw"
# hmset set多个key-value
127.0.0.1:6379> hmset myhash age 16 address glassstreet
OK
# hgetall 获取全部的数据
127.0.0.1:6379> hgetall myhash
1) "name"
2) "claw"
3) "age"
4) "16"
5) "address"
6) "glassstreet"
# hdel 删除hash指定的key字段,对应的value也会被删除掉.
127.0.0.1:6379> hdel myhash address
(integer) 1
127.0.0.1:6379> hgetall myhash
1) "name"
2) "claw"
3) "age"
4) "16"
####################################################################################
# 获取hash的长度
127.0.0.1:6379> hgetall myhash
1) "name"
2) "claw"
3) "age"
4) "16"
# hlen获取hash的长度
127.0.0.1:6379> hlen myhash
(integer) 2
####################################################################################
# 判断hash中某个key是否存在
127.0.0.1:6379> hexists myhash name
(integer) 1
127.0.0.1:6379> hexists myhash world
(integer) 0
####################################################################################
# hkeys 获得所有的field
127.0.0.1:6379> hkeys myhash
1) "name"
2) "age"
# hvals 获取所有的value
127.0.0.1:6379> hvals myhash
1) "claw"
2) "16"
####################################################################################
Zset
Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
有序集合的成员是唯一的,但分数(score)却可以重复。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。 集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)。
# zadd 在zset里插入数据
127.0.0.1:6379> zadd myzset 1 zhangsan
(integer) 1
127.0.0.1:6379> zadd myzset 2 lisi
(integer) 1
127.0.0.1:6379> zadd myzset 3 wangwu
(integer) 1
127.0.0.1:6379> zcard myset
(integer) 0
127.0.0.1:6379> zcard myzset
(integer) 3
# 展示所有zset里的数据
127.0.0.1:6379> zrange myzset 0 -1
1) "zhangsan"
2) "lisi"
3) "wangwu"
# 展示所有的zset数据并通过socre进行排序 -inf 负无穷 +inf 正无穷
127.0.0.1:6379> zrangebyscore myzset -inf +inf
1) "zhangsan"
2) "lisi"
3) "wangwu"
127.0.0.1:6379> zrangebyscore myzset -inf +inf withscores
1) "zhangsan"
2) "1"
3) "lisi"
4) "2"
5) "wangwu"
6) "3"
127.0.0.1:6379> zrangebyscore myzset -inf 2 withscores
1) "zhangsan"
2) "1"
3) "lisi"
4) "2"
三种特殊数据类型
geospatial 地理位置
有没有想过朋友的定位,附近的人,打车距离计算是如何实现的?
Redis在3.2版本中加入了geospatial以及索引半径查询功能,这在需要地理位置的应用上或许可以一展身手。
相关命令:
- GEOADD
- GEODIST
- GEOHASH
- GEOPOS
- GEORADIUS
-
GEORADIUSBYMEMBER
GEOADD key member [member ...]
将指定的地理空间位置(经度、纬度、名称)添加到指定的key
中。这些数据将会存储到sorted set
这样的目的是为了方便使用GEORADIUS或者GEORADIUSBYMEMBER命令对数据进行半径查询等操作。
该命令以采用标准格式的参数x,y,所以经度必须在纬度之前。这些坐标的限制是可以被编入索引的,区域面积可以很接近极点但是不能索引。具体的限制,由EPSG:900913 / EPSG:3785 / OSGEO:41001 规定如下:
- 有效的经度从-180度到180度。
- 有效的纬度从-85.05112878度到85.05112878度。
当坐标位置超出上述指定范围时,该命令将会返回一个错误。
实例(一)GEO添加地理位置
从网上查询数据得到城市经纬度
北京市 经度:116.40 纬度:39.90 天津市 经度:117.20 纬度:39.12 上海市 经度:121.47 纬度:31.23
# getadd 添加地理位置
127.0.0.1:6379> geoadd china:city 116.40 39.90 beijing
(integer) 1
127.0.0.1:6379> geoadd china:city 117.20 39.12 tianjing
(integer) 1
127.0.0.1:6379> geoadd china:city 121.47 31.23 shanghai
(integer) 1
GEOPOS key member [member ...]
从key里返回所有给定位置元素的位置(经度和纬度)
GEOPOS 命令返回一个数组, 数组中的每个项都由两个元素组成: 第一个元素为给定位置元素的经度, 而第二个元素则为给定位置元素的纬度。
当给定的位置元素不存在时, 对应的数组项为空值。
实例(一)查询北京和天津的经纬度
# 查询北京的经纬度
127.0.0.1:6379> geopos china:city beijing
1) 1) "116.39999896287918"
2) "39.900000091670925"
# 查询天津的经纬度
127.0.0.1:6379> geopos china:city tianjing
1) 1) "117.19999998807907"
2) "39.120000488192183"
GEODIST key member1 member2 [unit]
返回两个给定位置之间的距离。
如果两个位置之间的其中一个不存在, 那么命令返回空值。
指定单位的参数 unit 必须是以下单位的其中一个:
- m 表示单位为米。
- km 表示单位为千米。
- mi 表示单位为英里。
- ft 表示单位为英尺。
如果用户没有显式地指定单位参数, 那么 GEODIST
默认使用米作为单位。
GEODIST
命令在计算距离时会假设地球为完美的球形, 在极限情况下, 这一假设最大会造成 0.5% 的误差。
# geodist 得到两个地区之间的距离
127.0.0.1:6379> geodist china:city beijing tianjing
"110631.2710"
# geodist 得到两个地区之间的距离 以km的单位显示
127.0.0.1:6379> geodist china:city beijing tianjing km
"110.6313"
127.0.0.1:6379>
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count]
以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。
范围可以使用以下其中一个单位:
- m 表示单位为米。
- km 表示单位为千米。
- mi 表示单位为英里。
- ft 表示单位为英尺。
在给定以下可选项时, 命令会返回额外的信息:
WITHDIST
: 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。 距离的单位和用户给定的范围单位保持一致。WITHCOORD
: 将位置元素的经度和维度也一并返回。WITHHASH
: 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大。
命令默认返回未排序的位置元素。 通过以下两个参数, 用户可以指定被返回位置元素的排序方式:
ASC
: 根据中心的位置, 按照从近到远的方式返回位置元素。DESC
: 根据中心的位置, 按照从远到近的方式返回位置元素。
在默认情况下, GEORADIUS 命令会返回所有匹配的位置元素。 虽然用户可以使用 COUNT <count>
选项去获取前 N 个匹配元素, 但是因为命令在内部可能会需要对所有被匹配的元素进行处理, 所以在对一个非常大的区域进行搜索时, 即使只使用 COUNT
选项去获取少量元素, 命令的执行速度也可能会非常慢。 但是从另一方面来说, 使用 COUNT
选项去减少需要返回的元素数量, 对于减少带宽来说仍然是非常有用的。
# 获取 116 39 这个经纬度为中心 寻找1000km内的城市
# 所有的数据都应该录入到 china:city里,才会让结果更加清晰
127.0.0.1:6379> georadius china:city 116 39 1000 km
1) "tianjing"
2) "beijing"
3) "shanghai"
# 显示到中心距离的位置
127.0.0.1:6379> georadius china:city 116 39 1000 km withdist
1) 1) "tianjing"
2) "104.4940"
2) 1) "beijing"
2) "105.8343"
3) 1) "shanghai"
2) "996.7785"
# count 1 :只显示一条(如果周围的数据很多,难道都要显示吗? count 可以限制数量)
127.0.0.1:6379> georadius china:city 116 39 1000 km withdist count 1
1) 1) "tianjing"
2) "104.4940"
# 显示周围城市的经纬度
127.0.0.1:6379> georadius china:city 116 39 1000 km withcoord
1) 1) "tianjing"
2) 1) "117.19999998807907"
2) "39.120000488192183"
2) 1) "beijing"
2) 1) "116.39999896287918"
2) "39.900000091670925"
3) 1) "shanghai"
2) 1) "121.47000163793564"
2) "31.22999
GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count]
这个命令和 GEORADIUS 命令一样, 都可以找出位于指定范围内的元素, 但是 GEORADIUSBYMEMBER
的中心点是由给定的位置元素决定的, 而不是像 GEORADIUS 那样, 使用输入的经度和纬度来决定中心点
指定成员的位置被用作查询的中心。
# 找出以北京为中心 周围500km的城市
127.0.0.1:6379> georadiusbymember china:city beijing 500 km
1) "tianjing"
2) "beijing"
GeoHash
该命令将返回11个字符的Geohash字符串
将二维的经纬度转换为一维的字符串,如果两个字符串越接近,那么则距离越近。
127.0.0.1:6379> geohash china:city beijing
1) "wx4fbxxfke0"
127.0.0.1:6379> geohash china:city tianjing
1) "wwgqdcw6tb0"
Geo底层实现原理是Zset,所以我们可以使用Zset命令才操作Geo
比如使用zrange查看所有元素,同理也删除命令也能使用~
127.0.0.1:6379> zrange china:city 0 -1
1) "shanghai"
2) "tianjing"
3) "beijing"
Hyperloglog 基数统计
(待更新)
BitMap 位图场景
(待更新)
Redis的事务
Redis单条命令是保证原子性的,但是事务不保证原子性。
Redis事务的本质:一组命令的集合,一个集合中的所有命令都会被序列化,在事务执行的过程中,会按顺序执行。
它有一次性,顺序性,排他性,执行一系列的命令。
--------------------------------------队列 set set set 执行--------------------------------------
Redis事务没有隔离级别的概念,也就是它就没有一系列关系型数据库里的的概念,比如脏读,不可重复读等。所有的命令在事务中,并没有直接执行,只有发起执行命令时候才会执行。
redis的事务:
- 开启事务 (multi)
- 命令入队(.....)
- 执行事务
实例(一),正常执行事务。
# multi 开启事务
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> get k3
QUEUED
# 执行事务
127.0.0.1:6379> exec
1) OK
2) OK
3) OK
4) "v3"
实例(二),放弃事务
# 开启事务
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
# 放弃事务所以set操作并未成功,此时get k3显示为空
127.0.0.1:6379> get k3
(nil)
127.0.0.1:6379>
如果期间遇到错误怎么办呢?在java中,有编译型的异常以及运行时异常,在Redis里也一样,编译型异常都是代码出现了问题,也就说命令出了问题,事务中所有的命令都不会执行。运行时的异常,在如果事务队列中存在语法型错误,那么执行命令的时候,其他命令是可以正常执行的,错误命令会抛出异常。
实例(三)编译期间出现错误的命令,可以看到当编译期存在错误的指令的时候,事务所有的命令都没有被执行。
# 开启事务
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v3
QUEUED
127.0.0.1:6379> set k3 v2
QUEUED
# 一条错误指令
127.0.0.1:6379> getset k3
(error) ERR wrong number of arguments for 'getset' command
# 仍然执行
127.0.0.1:6379> exec
# 报错信息 由于先前的错误,EXECABORT事务被丢弃。
(error) EXECABORT Transaction discarded because of previous errors.
# get k1 的值,没有set成功
127.0.0.1:6379> get k1
(nil)
实例(四)运行期间出现错误的命令其他命令能够运行成功,错误的指令报错,这也是redis事务的不保证原子性。
# 开启事务
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
# 让k1自增 因为k1的value是字符串 无法自增 因此这是个错误的指令 但不是编译期的错误
127.0.0.1:6379> incr k1
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
# 执行
127.0.0.1:6379> exec
1) OK
2) OK
# 虽然这条命令报错,但其他的执行成功了
3) (error) ERR value is not an integer or out of range
4) OK
Redis实现乐观锁
悲观锁和乐观锁
- 悲观锁
- 很悲观,悲观锁认为任何时候都有可能会出现问题,无论做什么都会加锁。
- 乐观锁
- 很乐观,乐观锁任何时候都不会出现问题,所以不会加锁。更新数据的时候去判断一下,在此期间是否有人改动过数据。
- 获取version
- 更新的时候比较version
实例(一) Redis监视测试,模拟一次动账,money为现有金额,out为花出去的金额。监视money,可以看到事务正常结束,数据期间没有发生过变动(也就是在执行的时候没有其他线程改变money的值),这样这场事务就执行成功了。
# 模拟一次动账
# 设置money为10
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK
# 监视money
127.0.0.1:6379> watch money
OK
# 开启事务
127.0.0.1:6379> multi
OK
# money -10
127.0.0.1:6379> decrby money 10
QUEUED
# out +10
127.0.0.1:6379> incrby out 10
QUEUED
# 执行
127.0.0.1:6379> exec
1) (integer) 90
2) (integer) 10
实例(二):测试多线程修改值
仍然是刚刚的例子,此时我们再次监视money,开启事务,并且让money -10。
使用watch可以当做是redis的乐观锁操作
# 监视money
127.0.0.1:6379> watch money
OK
# 开启事务
127.0.0.1:6379> multi
OK
# 进入队列
127.0.0.1:6379> decrby money 10
QUEUED
此时开启另外的线程,设置money 为1000
# 设置money为1000
127.0.0.1:6379> set money 1000
OK
然后回到刚刚观察的线程上,执行事务,执行失败了。
# 监视money
127.0.0.1:6379> watch money
OK
# 开启事务
127.0.0.1:6379> multi
OK
# 进入队列
127.0.0.1:6379> decrby money 10
QUEUED
# 执行事务,执行之前另外一个线程修改了我们的值,就会导致事务执行失败。
127.0.0.1:6379> exec
(nil)
实例(三)事务执行失败的解决方式:如果发现事务执行失败,就使用unwatch解锁,然后再次获取锁。
# 如果发现事务执行失败,就使用unwtach解锁
127.0.0.1:6379> unwatch
OK
# 查看现在money的值
127.0.0.1:6379> get money
"1000"
# 再次监视money
127.0.0.1:6379> watch money
OK
# 开启事务
127.0.0.1:6379> multi
OK
# money -99
127.0.0.1:6379> decrby money 99
QUEUED
# 执行事务
127.0.0.1:6379> exec
1) (integer) 901