1. Redis 概述
1.1 互联网架构的演变历程
1.1.1 第一阶段
- 数据访问量不大,简单的架构即可搞定
1.1.2 第二阶段
-
数据访问量大,使用缓存技术来缓解数据库的压力
-
不同的业务访问不同的数据库
1.1.3 第三阶段
-
主从读写分离
-
之前的缓存确实能够缓解数据库的压力,但是写和读都集中在一个数据库上,压力又来了
-
一个数据库负责写,一个数据库负责读
-
让 master(主数据库)来响应事务性(增删改)操作,让 slave(从数据库)来响应非事务性(查询)操作,然后再采用主从复制来把 master 上的事务性操作同步到 slave 数据库中
-
mysql 的 master/slave 就是网站的标配!
1.1.4 第四阶段
-
mysql 的主从复制,读写分离的基础上,mysql 的主库开始出现瓶颈
-
由于 MyISAM 使用表锁,所以并发性能特别差
-
分库分表开始流行,mysql 也提出了表分区,虽然不稳定,但我们看到了希望
-
开始 mysql 集群
1.2 Redis 入门介绍
1.2.1 互联网需求的三高
- 高并发、高可扩、高性能
1.2.2 Redis 是什么
-
Redis 是一种运行速度很快,并发性能很强,并且运行在内存上的 NoSql(not only sql)数据库
-
NoSQL 数据库和传统数据库相比的优势
- NoSQL 数据库无需事先为要存储的数据建立字段,随时可以存储自定义的数据格式
- 而在关系数据库里,增删字段是一件非常麻烦的事情,如果是非常大数据量的表,增加字段简直就是一个噩梦
1.2.3 Redis 使用场景
- 缓存,在提升服务器性能方面非常有效;一些频繁被访问的数据,如果放在关系型数据库,每次查询的开销都会很大,而放在 Redis 中,因为 Redis 是放在内存中的,可以很高效的访问;
- 排行,在使用传统的关系型数据库(mysql oracle 等)来做这个事儿,非常的麻烦,而利用 Redis 的 SortSet(有序集合)数据结构能够简单的搞定;
- 计算器 / 限速器,利用 Redis 中原子性的自增操作,我们可以统计类似用户点赞数、用户访问数等,这类操作如果用 MySQL,频繁的读写会带来相当大的压力;限速器比较典型的使用场景是限制某个用户访问某个 API 的频率,常用的有抢购时,防止用户疯狂点击带来不必要的压力(比如1秒内不论点击多少次,只有2次有效);
- 好友关系,利用集合的一些命令,比如求交集、并集、差集等,可以方便搞定一些共同好友、共同爱好之类的功能;
- 简单消息队列,除了 Redis 自身的发布/订阅模式,我们也可以利用 List 来实现一个队列机制,比如:到货通知、邮件发送之类的需求,不需要高可靠,但是会带来非常大的 DB 压力,完全可以用 List 来完成异步解耦;
- Session 共享,以 jsp 为例,默认 Session 是保存在服务器的文件中,如果是集群服务,同一个用户过来可能落在不同机器上,这就会导致用户频繁登陆;采用 Redis 保存 Session 后,无论用户落在那台机器上都能够获取到对应的 Session 信息。
1.3 Redis/Memcache/MongoDB 对比
1.3.1 Redis 和 Memcache
- Redis 和 Memcache 都是内存数据库
- 不过 Memcache 还可用于缓存其他东西,例如图片、视频等等
- 数据结构
- Memcache 数据结构单一 kv;Redis 更丰富一些,还提供 list,set, hash 等数据结构的存储,有效减少网络 IO 的次数
- 虚拟内存
- Redis 当物理内存用完时,可以将一些很久没用到的 value 交换到磁盘
- 存储数据安全
- Memcache 挂掉后,数据没了(没有持久化机制);Redis 可以定期保存到磁盘(持久化)
- 灾难恢复
- Memcache 挂掉后,数据不可恢复; Redis 数据丢失后可以通过 RBD 或 AOF 恢复
1.3.2 Redis 和 MongoDB
- Redis 和 MongoDB 并不是竞争关系,更多的是一种协作共存的关系
- MongoDB 本质上还是硬盘数据库,在复杂查询时仍然会有大量的资源消耗,而且在处理复杂逻辑时仍然要不可避免地进行多次查询
- 这时就需要 Redis 或 Memcache 这样的内存数据库来作为中间层进行缓存和加速
- 比如在某些复杂页面的场景中,整个页面的内容如果都从 MongoDB 中查询,可能要几十个查询语句,耗时很长;如果需求允许,则可以把整个页面的对象缓存至 Redis 中,定期更新,这样 MongoDB 和 Redis 就能很好地协作起来
1.4 分布式数据库 CAP 原理
1.4.1 CAP 简介
- 传统的关系型数据库事务具备 ACID
- A:原子性
- C:一致性
- I :独立性
- D:持久性
- 分布式数据库的 CAP
- C(Consistency):强一致性
- ***“all nodes see the same data at the same time”***,即更新操作成功并返回客户端后,所有节点在同一时间的数据完全一致,这就是分布式的一致性。一致性的问题在并发系统中不可避免,对于客户端来说,一致性指的是并发访问时更新过的数据如何获取的问题。从服务端来看,则是更新如何复制分布到整个系统,以保证数据最终一致。
- A(Availability):高可用性
- 可用性指 **“Reads and writes always succeed”,即服务一直可用,而且要是正常的响应时间。好的可用性主要是指系统能够很好的为用户服务,不出现用户操作失败或者访问超时等用户体验不好的情况。
- P(Partition tolerance):分区容错性
- 即分布式系统在遇到某节点或网络分区故障时,仍然能够对外提供满足一致性或可用性的服务。
- 分区容错性要求能够使应用虽然是一个分布式系统,而看上去却好像是在一个可以运转正常的整体。比如现在的分布式系统中有某一个或者几个机器宕掉了,其他剩下的机器还能够正常运转满足系统需求,对于用户而言并没有什么体验上的影响。
- C(Consistency):强一致性
1.4.2 CAP 理论
-
CAP 理论提出就是针对分布式数据库环境的,所以,P 这个属性必须容忍它的存在,而且是必须具备的
-
因为 P 是必须的,那么我们需要选择的就是 A 和 C
-
在分布式环境下,为了保证系统可用性,通常都采取了复制的方式,避免一个节点损坏,导致系统不可用。那么就出现了每个节点上的数据出现了很多个副本的情况,而数据从一个节点复制到另外的节点时需要时间和要求网络畅通的,所以,当 P 发生时,也就是无法向某个节点复制数据时,这时候你有两个选择:
- 选择可用性 A,此时,那个失去联系的节点依然可以向系统提供服务,不过它的数据就不能保证是同步的了(失去了 C 属性)
- 选择一致性 C,为了保证数据库的一致性,我们必须等待失去联系的节点恢复过来,在这个过程中,那个节点是不允许对外提供服务的,这时候系统处于不可用状态(失去了 A 属性)
-
最常见的例子是读写分离,某个节点负责写入数据,然后将数据同步到其它节点,其它节点提供读取的服务,当两个节点出现通信问题时,你就面临着选择A(继续提供服务,但是数据不保证准确),C(用户处于等待状态,一直等到数据同步完成)
1.4.3 CAP 总结
- 分区是常态,不可避免,三者不可共存
- 可用性和一致性是一对冤家
- 一致性高,可用性低
- 一致性低,可用性高
- 因此,根据 CAP 原理将 NoSQL 数据库分成了满足 CA 原则、满足 CP 原则和满足 AP 原则三大类
- CA - 单点集群,满足一致性,可用性的系统,通常在可扩展性上不太强大
- CP - 满足一致性,分区容忍性的系统,通常性能不是特别高
- AP - 满足可用性,分区容忍性的系统,通常可能对一致性要求低一些
2. 下载与安装
2.1 下载
- redis:http://www.redis.net.cn/
- 图形工具:https://redisdesktop.com/download
2.2 安装
虽然可以在安装在 windows 操作系统,但是官方不推荐,所以我们一如既往的安装在 linux 上
-
上传 tar.gz 包,并解压
cd /opt tar -zxvf redis-5.0.4.tar.gz
-
安装 gcc(必须有网络)
yum -y install gcc
-
进入 redis 目录,进行编译 + 安装
cd redis-5.0.4/ make make install
2.3 安装后的操作
2.3.1 后台运行方式
启动进入文件夹 cd /usr/local/bin
-
直接启动:
redis-server
,窗口会被占用,不能输入其他命令 -
后台运行:修改配置文件
daemonize=yes
,当你后台服务启动的时候,会写成一个进程文件运行vi /opt/redis-5.0.4/redis.conf #找到并修改daemonize daemonize yes #启动 redis-server /opt/redis-5.0.4/redis.conf
2.3.2 关闭数据库
- 单实例关闭
redis-cli shutdown
- 多实例关闭(更彻底)
redis-cli -p 6379 shutdown
2.3.3 常用操作
- 检测 6379 端口是否在监听
netstat -lntp | grep 6379
- 检测后台进程是否存在
ps -ef|grep redis
2.3.4 连接 redis 并测试
redis-cli
ping #pong
2.3.5 HelloWorld
# 保存数据
set k1 china
# 获取数据
get kl
2.3.6 测试性能
- 先 ctrl+c,退出 redis 客户端
- 执行
redis-benchmark
命令后,命令不会自动停止,需要我们手动 ctrl+c 停止测试
[root@localhost bin]# redis-benchmark
====== PING_INLINE ======
100000 requests completed in 1.80 seconds # 1.8秒处理了10万个请求,性能要看笔记本的配置高低
50 parallel clients
3 bytes payload
keep alive: 1
87.69% <= 1 milliseconds
99.15% <= 2 milliseconds
99.65% <= 3 milliseconds
99.86% <= 4 milliseconds
99.92% <= 5 milliseconds
99.94% <= 6 milliseconds
99.97% <= 7 milliseconds
100.00% <= 7 milliseconds
55524.71 requests per second # 每秒处理的请求数量
2.3.7 默认 16 个数据库
vim /opt/redis-5.0.4/redis.conf
127.0.0.1:6379> get k1 # 查询k1
"china"
127.0.0.1:6379> select 16 # 切换16号数据库
(error) ERR DB index is out of range # 数据库的下标超出了范围
127.0.0.1:6379> select 15 # 切换15号数据库
OK
127.0.0.1:6379[15]> get k1 # 查询k1
(nil) # 空
127.0.0.1:6379[15]> select 0 # 切换0号数据库
OK
127.0.0.1:6379> get k1 # 查询k1
"china"
2.3.8 数据库键的数量
dbsize
2.3.9 清空数据库
- 清空当前库
flushdb
- 清空所有(16个)库,慎用!!
flushall
2.3.10 模糊查询(key)
模糊查询 keys 命令,有三个通配符:
*:通配任意多个字符
- 查询所有的键
keys *
- 模糊查询 k 开头,后面随便多少个字符
keys k*
- 模糊查询 e 为最后一位,前面随便多少个字符
keys *e
- 双 * 模式,查询包含 k 的键
keys *k*
?:通配单个字符
- 模糊查询 k 字头,并且匹配一个字符
keys k?
- 你只记得第一个字母是 k,他的长度是 3
keys k??
[]:通配括号内的某一个字符
- 记得其他字母,就第二个字母可能是 a 或 e
keys r[ae]dis
2.3.11 键(key)
exists key
:判断某个key是否存在
127.0.0.1:6379> exists k1
(integer) 1 # 存在
127.0.0.1:6379> exists y1
(integer) 0 # 不存在
move key db
:移动(剪切,粘贴)键到几号库
127.0.0.1:6379> move x1 8 # 将x1移动到8号库
(integer) 1 # 移动成功
127.0.0.1:6379> exists x1 # 查看当前库中是否存在x1
(integer) 0 # 不存在(因为已经移走了)
127.0.0.1:6379> select 8 # 切换8号库
OK
127.0.0.1:6379[8]> keys * # 查看当前库中的所有键
1) "x1"
ttl key
:查看键还有多久过期(-1永不过期,-2已过期)—— time to live 还能活多久
127.0.0.1:6379[8]> ttl x1
(integer) -1 # 永不过期
expire key 秒
:为键设置过期时间(生命倒计时)
127.0.0.1:6379[8]> set k1 v1 # 保存k1
OK
127.0.0.1:6379[8]> ttl k1 # 查看k1的过期时间
(integer) -1 # 永不过期
127.0.0.1:6379[8]> expire k1 10 # 设置k1的过期时间为10秒(10秒后自动销毁)
(integer) 1 # 设置成功
127.0.0.1:6379[8]> get k1 # 获取k1
"v1"
127.0.0.1:6379[8]> ttl k1 # 查看k1的过期时间
(integer) 2 # 还有2秒过期
127.0.0.1:6379[8]> get k1
(nil)
127.0.0.1:6379[8]> keys * # 从内存中销毁了
(empty list or set)
type key
:查看键的数据类型
127.0.0.1:6379[8]> type k1
string # k1的数据类型是会string字符串
3. 使用 Redis
操作文档:http://redisdoc.com/
3.1 五大数据类型
3.1.1 字符串 String
- set / get / del / append / strlen
127.0.0.1:6379> set k1 v1 # 保存数据
OK
127.0.0.1:6379> set k2 v2 # 保存数据
OK
127.0.0.1:6379> keys *
1) "k1"
2) "k2"
127.0.0.1:6379> del k2 # 删除数据k2
(integer) 1 # 删除成功
127.0.0.1:6379> keys *
1) "k1"
127.0.0.1:6379> get k1 # 获取数据k1
"v1"
127.0.0.1:6379> append k1 abc # 往k1的值追加数据abc
(integer) 5 # 返回值的长度(字符数量)
127.0.0.1:6379> get k1
"v1abc"
127.0.0.1:6379> strlen k1 # 返回k1值的长度(字符数量)
(integer) 5
- incr / decr / incrby / decrby:加减操作,操作的必须是数字类型
- incr:意思是 increment,增加
- decr:意思是 decrement,减少
127.0.0.1:6379> set k1 1 # 初始化k1的值为1
OK
127.0.0.1:6379> incr k1 # k1自增1(相当于++)
(integer) 2
127.0.0.1:6379> incr k1
(integer) 3
127.0.0.1:6379> get k1
"3"
127.0.0.1:6379> decr k1 # k1自减1(相当于--)
(integer) 2
127.0.0.1:6379> decr k1
(integer) 1
127.0.0.1:6379> get k1
"1"
127.0.0.1:6379> incrby k1 3 # k1自增3(相当于+=3)
(integer) 4
127.0.0.1:6379> get k1
"4"
127.0.0.1:6379> decrby k1 2 # k1自减2(相当于-=2)
(integer) 2
127.0.0.1:6379> get k1
"2"
- getrange / setrange:类似 between…and…
127.0.0.1:6379> set k1 abcdef # 初始化k1的值为abcdef
OK
127.0.0.1:6379> get k1
"abcdef"
127.0.0.1:6379> getrange k1 0 -1 # 查询k1全部的值
"abcdef"
127.0.0.1:6379> getrange k1 0 3 # 查询k1的值,范围是下标0~下标3(包含0和3,共返回4个字符)
"abcd"
127.0.0.1:6379> setrange k1 1 xxx # 替换k1的值,从下标1开始提供为xxx
(integer) 6
127.0.0.1:6379> get k1
"axxxef"
- setex / setnx
- set with expir:添加数据的同时设置生命周期
- set if not exist:添加数据的时候判断是否已经存在,防止已存在的数据被覆盖掉
127.0.0.1:6379> setex k1 5 v1 # 添加k1 v1数据的同时,设置5秒的声明周期
OK
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379> get k1
(nil) # 已过期,k1的值v1自动销毁
127.0.0.1:6379> setnx k1 sun
(integer) 0 # 添加失败,因为k1已经存在
127.0.0.1:6379> get k1
"laosun"
127.0.0.1:6379> setnx k2 sun
(integer) 1 # k2不存在,所以添加成功
- mset / mget / msetnx
- m:more更多
127.0.0.1:6379> set k1 v1 k2 v2 # set不支持一次添加多条数据
(error) ERR syntax error
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 # mset可以一次添加多条数据
OK
127.0.0.1:6379> keys *
1) "k1"
2) "k2"
3) "k3"
127.0.0.1:6379> mget k2 k3 # 一次获取多条数据
1) "v2"
2) "v3"
127.0.0.1:6379> msetnx k3 v3 k4 v4 # 一次添加多条数据时,如果添加的数据中有已经存在的,则失败
(integer) 0
127.0.0.1:6379> msetnx k4 v4 k5 v5 # 一次添加多条数据时,如果添加的数据中都不存在的,则成功
(integer) 1
- getset:先 get 后 set
127.0.0.1:6379> getset k6 v6
(nil) # 因为没有k6,所以get为null,然后将k6v6的值添加到数据库
127.0.0.1:6379> keys *
1) "k4"
2) "k1"
3) "k2"
4) "k3"
5) "k5"
6) "k6"
127.0.0.1:6379> get k6
"v6"
127.0.0.1:6379> getset k6 vv6 # 先获取k6的值,然后修改k6的值为vv6
"v6"
127.0.0.1:6379> get k6
"vv6"
3.1.2 列表 List
push 和 pop,类似机枪 AK47:push,压子弹,pop,射击出子弹
- lpush / rpush / lrange
- l:left 自左向右→添加 (从上往下添加,最先添加的压到最下方)
- r:right 自右向左←添加(从下往上添加)
127.0.0.1:6379> lpush list01 1 2 3 4 5 # 从上往下添加
(integer) 5
127.0.0.1:6379> keys *
1) "list01"
127.0.0.1:6379> lrange list01 0 -1 # 查询list01中的全部数据0表示开始,-1表示结尾
1) "5"
2) "4"
3) "3"
4) "2"
5) "1"
127.0.0.1:6379> rpush list02 1 2 3 4 5 # 从下往上添加
(integer) 5
127.0.0.1:6379> lrange list02 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
- lpop / rpop:移除第一个元素(上左下右)
127.0.0.1:6379> lpop list02 # 从左(上)边移除第一个元素
"1"
127.0.0.1:6379> rpop list02 # 从右(下)边移除第一个元素
"5"
- lindex:根据下标查询元素(从左向右,自上而下)
127.0.0.1:6379> lrange list01 0 -1
1) "5"
2) "4"
3) "3"
4) "2"
5) "1"
127.0.0.1:6379> lindex list01 2 # 从上到下数,下标为2的值
"3"
127.0.0.1:6379> lindex list01 1 # 从上到下数,下标为1的值
"4"
- llen:返回集合长度
127.0.0.1:6379> llen list01
(integer) 5
- lrem:删除 n 个 value
127.0.0.1:6379> lpush list01 1 2 2 3 3 3 4 4 4 4
(integer) 10
127.0.0.1:6379> lrem list01 2 3 # 从list01中移除2个3
(integer) 2
127.0.0.1:6379> lrange list01 0 -1
1) "4"
2) "4"
3) "4"
4) "4"
5) "3"
6) "2"
7) "2"
8) "1"
- ltrim:截取指定范围的值,别的全扔掉
127.0.0.1:6379> lpush list01 1 2 3 4 5 6 7 8 9
(integer) 9
127.0.0.1:6379> lrange list01 0 -1
1) "9" # 下标0
2) "8" # 下标1
3) "7" # 下标2
4) "6" # 下标3
5) "5" # 下标4
6) "4" # 下标5
7) "3" # 下标6
8) "2" # 下标7
9) "1" # 下标8
127.0.0.1:6379> ltrim list01 3 6 # 截取下标3~6的值,别的全扔掉
OK
127.0.0.1:6379> lrange list01 0 -1
1) "6"
2) "5"
3) "4"
4) "3"
- rpoplpush:从一个集合搞一个元素到另一个集合中(右出一个,左进一个)
127.0.0.1:6379> rpush list01 1 2 3 4 5
(integer) 5
127.0.0.1:6379> lrange list01 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
127.0.0.1:6379> rpush list02 1 2 3 4 5
(integer) 5
127.0.0.1:6379> lrange list02 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
127.0.0.1:6379> rpoplpush list01 list02 # list01右边出一个,从左进入到list02的第一个位置
"5"
127.0.0.1:6379> lrange list01 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
127.0.0.1:6379> lrange list02 0 -1
1) "5"
2) "1"
3) "2"
4) "3"
5) "4"
6) "5"
- lset:改变某个下标的某个值
127.0.0.1:6379> lrange list02 0 -1
1) "5"
2) "1"
3) "2"
4) "3"
5) "4"
6) "5"
127.0.0.1:6379> lset list02 0 x # 将list02中下标为0的元素修改成x
OK
127.0.0.1:6379> lrange list02 0 -1
1) "x"
2) "1"
3) "2"
4) "3"
5) "4"
6) "5"
- linsert:插入元素(指定某个元素(非下标)之前/之后)
127.0.0.1:6379> lrange list02 0 -1
1) "x"
2) "1"
3) "2"
4) "3"
5) "4"
6) "5"
127.0.0.1:6379> linsert list02 before 2 java # 从左边进入,在list02中的2元素之前插入java
(integer) 7
127.0.0.1:6379> lrange list02 0 -1
1) "x"
2) "1"
3) "java"
4) "2"
5) "3"
6) "4"
7) "5"
127.0.0.1:6379> linsert list02 after 2 redis # 从左边进入,在list02中的2元素之后插入redis
(integer) 8
127.0.0.1:6379> lrange list02 0 -1
1) "x"
2) "1"
3) "java"
4) "2"
5) "redis"
6) "3"
7) "4"
8) "5"
性能总结:类似添加火车皮一样,头尾操作效率高,中间操作效率惨;
3.1.3 集合 Set
和 Java中的 Set 特点类似,不允许重复
- sadd / smembers / sismember:添加/查看/判断是否存在
- 注意:1 和 0 可不是下标,而是布尔
- 1:true 存在,0:false 不存在
- 注意:1 和 0 可不是下标,而是布尔
127.0.0.1:6379> sadd set01 1 2 2 3 3 3 # 添加元素(自动排除重复元素)
(integer) 3
127.0.0.1:6379> smembers set01 # 查询set01集合
1) "1"
2) "2"
3) "3"
127.0.0.1:6379> sismember set01 2
(integer) 1 # 存在
127.0.0.1:6379> sismember set01 5
(integer) 0 # 不存在
- scard:获得集合中的元素个数
127.0.0.1:6379> scard set01
(integer) 3 # 集合中有3个元素
- srem:删除集合中的元素
127.0.0.1:6379> srem set01 2 # 移除set01中的元素2
(integer) 1 # 1表示移除成功
- srandmember:从集合中随机获取几个元素
127.0.0.1:6379> sadd set01 1 2 3 4 5 6 7 8 9
(integer) 9
127.0.0.1:6379> smembers set01
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
7) "7"
8) "8"
9) "9"
127.0.0.1:6379> srandmember set01 3 # 从set01中随机获取3个元素
1) "8"
2) "2"
3) "3"
127.0.0.1:6379> srandmember set01 5 # 从set01中随机获取5个元素
1) "5"
2) "8"
3) "7"
4) "4"
5) "6"
- spop:随机出栈(移除)
127.0.0.1:6379> smembers set011) "1"2) "2"3) "3"4) "4"5) "5"6) "6"7) "7"8) "8"9) "9"127.0.0.1:6379> spop set01 # 随机移除一个元素"8"127.0.0.1:6379> spop set01 # 随机移除一个元素"7"127.0.0.1:6379> smembers set011) "1"2) "2"3) "3"4) "4"5) "5"6) "6"7) "9"
- smove:移动元素:将 key1 某个值赋值给 key2
127.0.0.1:6379> sadd set01 1 2 3 4 5(integer) 5127.0.0.1:6379> sadd set02 x y z(integer) 3127.0.0.1:6379> smove set01 set02 3 # 将set01中的元素3移动到set02中(integer) 1 # 移动成功127.0.0.1:6379> smembers set011) "1"2) "2"3) "4"4) "5"127.0.0.1:6379> smembers set021) "z"2) "y"3) "3"4) "x"
- 数学集合类
- 交集:sinter
- 并集:sunion
- 差集:sdiff
127.0.0.1:6379> sadd set01 1 2 3 4 5
(integer) 5
127.0.0.1:6379> sadd set02 2 a 1 b 3
(integer) 5
127.0.0.1:6379> sinter set01 set02 # set01和set02共同存在的元素
1) "1"
2) "2"
3) "3"
127.0.0.1:6379> sunion set01 set02 # 将set01和set02中所有元素合并起来(排除重复的)
1) "5"
2) "4"
3) "3"
4) "2"
5) "b"
6) "a"
7) "1"
127.0.0.1:6379> sdiff set01 set02 # 在set01中存在,在set02中不存在
1) "4"
2) "5"
127.0.0.1:6379> sdiff set02 set01 # 在set02中存在,在set01中不存在
1) "b"
2) "a"
3.1.4 哈希 Hash
类似 Java里面的 Map<String,Object>,KV 模式不变,但 V 是一个键值对(可存多字段)
- hset / hget / hmset / hmget / hgetall / hdel:添加/得到/多添加/多得到/得到全部/删除属性
127.0.0.1:6379> hset user id 1001 # 添加user,值为id=1001
(integer) 1
127.0.0.1:6379> hget user
(error) ERR wrong number of arguments for 'hget' command
127.0.0.1:6379> hget user id # 查询user,必须指明具体的字段
"1001"
127.0.0.1:6379> hmset student id 101 name tom age 22 # 添加学生student,属性一堆
OK
127.0.0.1:6379> hget student name # 获取学生名字
"tom"
127.0.0.1:6379> hmget student name age # 获取学生年龄
1) "tom"
2) "22"
127.0.0.1:6379> hgetall student # 获取学生全部信息
1) "id"
2) "101"
3) "name"
4) "tom"
5) "age"
6) "22"
127.0.0.1:6379> hdel student age # 删除学生年龄属性
(integer) 1 # 删除成功
127.0.0.1:6379> hgetall student
1) "id"
2) "101"
3) "name"
4) "tom"
- hlen:返回元素的属性个数
127.0.0.1:6379> hgetall student1) "id"2) "101"3) "name"4) "tom"127.0.0.1:6379> hlen student(integer) 2 # student属性的数量,id和name,共两个属性
- hexists:判断元素是否存在某个属性
127.0.0.1:6379> hexists student name # student中是否存在name属性
(integer) 1 # 存在
127.0.0.1:6379> hexists student age # student中是否存在age属性
(integer) 0 # 不存在
- hkeys / hvals:获得属性的所有 key /获得属性的所有 value
127.0.0.1:6379> hkeys student # 获取student所有的属性名
1) "id"
2) "name"
127.0.0.1:6379> hvals student # 获取student所有属性的值(内容)
1) "101"
2) "tom"
- hincrby / hincrbyfloat:自增(整数)/自增(小数)
127.0.0.1:6379> hmset student id 101 name tom age 22
OK
127.0.0.1:6379> hincrby student age 2 # 自增整数2
(integer) 24
127.0.0.1:6379> hget student age
"24"
127.0.0.1:6379> hmset user id 1001 money 1000
OK
127.0.0.1:6379> hincrbyfloat user money 5.5 # 自增小数5.5
"1005.5"
127.0.0.1:6379> hget user money
"1005.5"
- hsetnx:添加的时候,先判断是否存在
127.0.0.1:6379> hsetnx student age 18 # 添加时,判断age是否存在
(integer) 0 # 添加失败,因为age已存在
127.0.0.1:6379> hsetnx student sex 男 # 添加时,判断sex是否存在
(integer) 1 # 添加成功,因为sex不存在
127.0.0.1:6379> hgetall student
1) "id"
2) "101"
3) "name"
4) "tom"
5) "age"
6) "24"
7) "sex"
8) "\xe7\x94\xb7" # 可以添加中文,但是显示为乱码(后期解决)
3.1.5 有序集合 Zset
真实需求:充10元可享vip1;充20元可享vip2;充30元可享vip3;以此类推…
- zadd / zrange (withscores):添加/查询
127.0.0.1:6379> zadd zset01 10 vip1 20 vip2 30 vip3 40 vip4 50 vip5
(integer) 5
127.0.0.1:6379> zrange zset01 0 -1 # 查询数据
1) "vip1"
2) "vip2"
3) "vip3"
4) "vip4"
5) "vip5"
127.0.0.1:6379> zrange zset01 0 -1 withscores # 带着分数查询数据
1) "vip1"
2) "10"
3) "vip2"
4) "20"
5) "vip3"
6) "30"
7) "vip4"
8) "40"
9) "vip5"
10) "50"
- zrangebyscore:模糊查询
- ( : 不包含
- limit:跳过几个截取几个
127.0.0.1:6379> zrangebyscore zset01 20 40 # 20 <= score <= 40
1) "vip2"
2) "vip3"
3) "vip4"
127.0.0.1:6379> zrangebyscore zset01 20 (40 # 20 <= score < 40
1) "vip2"
2) "vip3"
127.0.0.1:6379> zrangebyscore zset01 (20 (40 # 20 < score < 40
1) "vip3"
127.0.0.1:6379> zrangebyscore zset01 10 40 limit 2 2 # 10 <= score <= 40,共返回四个,跳过前2个,取2个
1) "vip3"
2) "vip4"
127.0.0.1:6379> zrangebyscore zset01 10 40 limit 2 1 # 20 <= score <= 40,共返回四个,跳过前2个,取1个
1) "vip3"
- zrem:删除元素
127.0.0.1:6379> zrem zset01 vip5 # 移除vip5
(integer) 1
- zcard / zcount / zrank / zscore:集合长度/范围内元素个数/得元素下标/通过值得分数
127.0.0.1:6379> zcard zset01 # 集合中元素的个数
(integer) 4
127.0.0.1:6379> zcount zset01 20 30 # 分数在20~30之间,共有几个元素
(integer) 2
127.0.0.1:6379> zrank zset01 vip3 # vip3在集合中的下标(从上向下)
(integer) 2
127.0.0.1:6379> zscore zset01 vip2 # 通过元素获得对应的分数
"20"
- zrevrank:逆序找下标(从下向上)
127.0.0.1:6379> zrevrank zset01 vip3
(integer) 1
- zrevrange:逆序查询
127.0.0.1:6379> zrange zset01 0 -1 # 顺序查询
1) "vip1"
2) "vip2"
3) "vip3"
4) "vip4"
127.0.0.1:6379> zrevrange zset01 0 -1 # 逆序查询
1) "vip4"
2) "vip3"
3) "vip2"
4) "vip1"
- zrevrangebyscore:逆序范围查找
127.0.0.1:6379> zrevrangebyscore zset01 30 20 # 逆序查询分数在30~20之间的 (注意,先写大值,再写小值)
1) "vip3"
2) "vip2"
127.0.0.1:6379> zrevrangebyscore zset01 20 30 # 如果小值在前,则结果为null
(empty list or set)
3.2 持久化
3.2.1 RDB(Redis DataBase)
- 在指定的时间间隔内,将内存中的数据集的快照写入磁盘
- 默认保存在 /usr/local/bin 中,文件名 dump.rdb
3.2.1.1 自动备份
- Redis 是内存数据库,当我们每次用完 Redis,关闭 linux 时,按道理来说,内存释放,Redis 中的数据也会随之消失
- 为什么我们再次启动 Redis 的时候,昨天的数据还在,并没有消失呢?
- 这是因为自动备份机制,每次关机时,Redis 会自动将数据备份到一个文件中 :/usr/local/bin/dump.rdb
-
默认的自动备份策略不利于我们测试,所以修改 redis.conf 文件中的自动备份策略
- 当然如果你只是用 Redis 的缓存功能,不需要持久化,那么你可以注释掉所有的 save 行来停用保存功能,可以直接一个空字符串来实现停用:save “”
vim redis.conf /SNAP # 搜索 save 900 1 # 900秒内,至少变更1次,才会自动备份 save 120 10 # 120秒内,至少变更10次,才会自动备份 save 60 10000 # 60秒内,至少变更10000次,才会自动备份
-
使用 shutdown 模拟关机 ,关机之前和关机之后,对比 dump.rdb 文件的更新时间
- 注意:当我们使用 shutdown 命令,Redis 会自动将数据库备份,所以,dump.rdb 文件创建时间更新了
-
开机启动 Redis,我们要在 120 秒内保存 10 条数据,再查看 dump.rdb 文件的更新时间(开两个终端窗口,方便查看)
-
120 秒内保存 10 条数据这一动作触发了备份指令,目前,dump.rdb 文件中保存了 10 条数据,将 dump.rdb 拷贝一份 dump10.rdb,此时两个文件中都保存 10 条数据
-
既然有数据已经备份了,那我们就肆无忌惮的将数据全部删除 flushall,再次 shutdown 关机
-
再次启动 Redis,发现数据真的消失了,并没有按照我们所想的将 dump.rdb 文件中的内容恢复到 Redis中。为什么?
- 因为,当我们保存 10 条以上的数据时,数据备份起来了;然后删除数据库,备份文件中的数据,也没问题;
- 但是,问题出在 shutdown 上,**这个命令一旦执行,就会立刻备份,将删除之后的空数据库生成备份文件,将之前装 10 条数据的备份文件覆盖掉了。**所以,就出现了上图的结果。自动恢复失败。
- 怎么解决这个问题呢?要将备份文件再备份。
-
将 dump.rdb 文件删除,将 dump10.rdb 重命名为 dump.rdb
-
启动 Redis 服务,登录 Redis,数据 10 条,全部恢复!
3.2.1.2 手动备份
- 之前自动备份,必须更改好多数据,例如上边,我们改变了十多条数据,才会自动备份
- 现在,我只保存一条数据,就想立刻备份,应该怎么做?
- 每次操作完成,执行命令 save 就会立刻备份
3.2.1.3 与 RDB 相关的配置
- stop-writes-on-bgsave-error:进水口和出水口,出水口发生故障与否
- yes:当后台备份时候反生错误,前台停止写入
- no:不管死活,就是往里怼
- rdbcompression:对于存储到磁盘中的快照,是否启动 LZF 压缩算法,一般都会启动,因为这点性能,多买一台电脑,完全搞定 N 个来回了。
- yes:启动
- no:不启动(不想消耗CPU资源,可关闭)
- rdbchecksum:在存储快照后,是否启动 CRC64 算法进行数据校验;
- 开启后,大约增加 10% 左右的 CPU 消耗;
- 如果希望获得最大的性能提升,可以选择关闭;
- dbfilename:快照备份文件名字
- dir:快照备份文件保存的目录,默认为当前目录
优势 / 劣势
- 优:适合大规模数据恢复,对数据完整性和一致行要求不高;
- 劣:一定间隔备份一次,意外 down 掉,就失去最后一次快照的所有修改
3.2.2 AOF(Append Only File)
- 以日志的形式记录每个写操作
- 将 Redis 执行过的写指令全部记录下来(读操作不记录)
- 只许追加文件,不可以改写文件
- Redis 在启动之初会读取该文件从头到尾执行一遍,这样来重新构建数据
3.2.2.1 开启AOF
-
为了避免失误,最好将 redis.conf 总配置文件备份一下,然后再修改内容如下:
appendonly yes appendfilename appendonly.aof
-
重新启动 redis,以新配置文件启动
redis-server /usr/local/redis5.0.4/redis.conf
-
连接 redis,加数据,删库,退出
-
查看当前文件夹多一个 aof 文件,看看文件中的内容,保存的都是写操作
- 文件中最后一句要删除,否则数据恢复不了
- 编辑这个文件,最后要 :wq! 强制执行
-
只需要重新连接,数据恢复成功
3.2.2.2 共存?谁优先?
我们查看 redis.conf 文件,AOF 和 RDB 两种备份策略可以同时开启,那系统会怎样选择?
-
动手试试,编辑 appendonly.aof,胡搞乱码,保存退出
-
启动 redis 失败(写入代码格式不对),所以是 AOF 优先载入来恢复原始数据!因为 AOF 比 RDB 数据保存的完整性更高!
-
修复 AOF 文件,杀光不符合 redis 语法规范的代码
reids-check-aof --fix appendonly.aof
3.2.2.3 与 AOF 相关的配置
- appendonly:开启 AOF 模式
- appendfilename:aof 的文件名字,最好别改!
- appendfsync:追写策略
- always:每次数据变更,就会立即记录到磁盘,性能较差,但数据完整性好
- everysec:默认设置,异步操作,每秒记录,如果一秒内宕机,会有数据丢失
- no:不追写
- no-appendfsync-on-rewrite:重写时是否运用 Appendfsync 追写策略;用默认 no 即可,保证数据安全性
- AOF 采用文件追加的方式,文件会越来越大,为了解决这个问题,增加了重写机制,redis 会自动记录上一次 AOF 文件的大小,当 AOF 文件大小达到预先设定的大小时,redis 就会启动 AOF 文件进行内容压缩,只保留可以恢复数据的最小指令集合
- auto-aof-rewrite-percentage:如果 AOF 文件大小已经超过原来的 100%,也就是一倍,才重写压缩
- auto-aof-rewrite-min-size:如果 AOF 文件已经超过了 64mb,才重写压缩
3.2.3 总结(如何选择?)
- RDB:只用作后备用途,建议 15 分钟备份一次就好
- AOF:
- 在最恶劣的情况下,也只丢失不超过 2 秒的数据,数据完整性比较高,但代价太大,会带来持续的 IO
- 对硬盘的大小要求也高,默认 64mb 太小了,企业级最少都是 5G 以上
- 后面要学习的 master/slave 才是新浪微博的选择!!
3.3 事务
-
可以一次执行多个命令,是一个命令组,一个事务中,所有命令都会序列化(排队),不会被插队
-
一个队列中,一次性,顺序性,排他性的执行一系列命令
-
三特性
- 隔离性:所有命令都会按照顺序执行,事务在执行的过程中,不会被其他客户端送来的命令打断
- 没有隔离级别:队列中的命令没有提交之前都不会被实际的执行,不存在“事务中查询要看到事务里的更新,事务外查询不能看到”这个头疼的问题
- 不保证原子性:冤有头债有主,如果一个命令失败,但是别的命令可能会执行成功,没有回滚
-
三步走
- 开启 multi
- 入队 queued
- 执行 exec
-
与关系型数据库事务相比
- multi:可以理解成关系型事务中的 begin
- exec :可以理解成关系型事务中的 commit
- discard :可以理解成关系型事务中的 rollback
3.3.1 一起生
开启事务,加入队列,一起执行,并成功
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> set k3 v3
QUEUED # 加入队列
127.0.0.1:6379> exec # 执行,一起成功!
1) OK
2) OK
3) "v2"
4) OK
3.3.2 一起死
放弃之前的操作,恢复到原来的值
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> set k1 v1111
QUEUED
127.0.0.1:6379> set k2 v2222
QUEUED
127.0.0.1:6379> discard # 放弃操作
OK
127.0.0.1:6379> get k1
"v1" # 还是原来的值
3.3.3 一粒老鼠屎坏一锅汤(”编译“时错误全回滚)
一句报错,全部取消,恢复到原来的值
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> setlalala # 一句报错
(error) ERR unknown command `setlalala`, with args beginning with:
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> keys * # 还是原来的值
1) "k2"
2) "k3"
3) "k1"
3.3.4 冤有头债有主(”执行“时错误只报该句的错误)
追究责任,谁的错,找谁去
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr k1 # 虽然v1不能++,但是加入队列并没有报错,类似java中的通过编译
QUEUED
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> set k5 v5
QUEUED
127.0.0.1:6379> exec
1) (error) ERR value is not an integer or out of range # 真正执行的时候,报错
2) OK # 成功
3) OK # 成功
127.0.0.1:6379> keys *
1) "k5"
2) "k1"
3) "k3"
4) "k2"
5) "k4"
3.3.5 watch 监控
测试:模拟收入与支出
- 正常情况下:
127.0.0.1:6379> set in 100 # 收入100元
OK
127.0.0.1:6379> set out 0 # 支出0元
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby in 20 # 收入-20
QUEUED
127.0.0.1:6379> incrby out 20 # 支出+20
QUEUED
127.0.0.1:6379> exec
1) (integer) 80
2) (integer) 20 # 结果,没问题!
- 特殊情况下:
127.0.0.1:6379> watch in # 监控收入in
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby in 20
QUEUED
127.0.0.1:6379> incrby out 20
QUEUED
127.0.0.1:6379> exec
(nil) # 在exec之前,我开启了另一个窗口(线程),对监控的in做了修改,所以本次的事务将被打断(失效),类似于“乐观锁”
- unwatch:取消 watch 命令对所有 key 的操作
- 一旦执行了 exec 命令,那么之前加的所有监控自动失效!
3.4 Redis 的发布订阅
- 进程间的一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息
- 订阅一个或多个频道
127.0.0.1:6379> subscribe cctv1 cctv5 cctv6 # 1.订阅三个频道
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "cctv1"
3) (integer) 1
1) "subscribe"
2) "cctv5"
3) (integer) 2
1) "subscribe"
2) "cctv6"
3) (integer) 3
1) "message" # 3.cctv5接收到推送过来的信息
2) "cctv5"
3) "NBA"
127.0.0.1:6379> publish cctv5 NBA # 2.另开一个窗口 发送消息给cctv5
(integer) 1
3.5 主从复制
- 就是 redis 集群的策略
- 配从(库)不配主(库):小弟可以选择谁是大哥,但大哥没有权利去选择小弟
- 读写分离:主机写,从机读
3.5.1 一主二从
-
准备三台服务器,并修改 redis.conf
bind 0.0.0.0 #允许任何ip访问 重启redis: redis-server /opt/redis5.0.4/redis.conf
-
启动三台 redis,并查看每台机器的角色,都是 master
info replication
-
测试开始
-
首先,将三个机器全都清空,第一台添加值
mset k1 v1 k2 v2
-
其余两台机器,复制(找大哥)
slaveof 192.168.204.141 6379
-
第一台再添加值
set k3 v3
-
-
思考:slave 之前的 k1 和 k2 是否能拿到?
- 可以获得,只要跟了大哥,之前的数据也会立刻同步
-
思考:slave 之后的 k3 是否能拿到?
- 可以获得,只要跟了大哥,数据会立刻同步
-
思考:同时添加 k4,结果如何?
- 主机(master)可以添加成功,从机(slave)失败,从机只负责读取数据,无权写入数据,这就是**“读写分离”**
-
思考:主机 shutdown,从机如何?
- 142 和 143 仍然是 slave,并显示他们的 master 已离线
-
思考:主机重启,从机又如何?
- 142 和 143 仍然是 slave,并显示他们的 master 已上线
-
思考:从机死了,主机如何?从机归来身份是否变化?
- 主机没有变化,只是显示少了一个 slave
- 主机和从机没有变化,而重启归来的从机自立门户成为了 master,不和原来的集群在一起了
3.5.2 血脉相传(继承的传递性)
- 一个主机理论上可以多个从机,但是这样的话,这个主机会很累
- 我们可以使用 Java 面向对象继承中的传递性来解决这个问题,减轻主机的负担
- 形成祖孙三代:
127.0.0.1:6379> slaveof 192.168.204.141 6379 # 142跟随141
OK
127.0.0.1:6379> slaveof 192.168.204.142 6379 # 143跟随142
OK
3.5.3 谋权篡位(升权)
- 1 个主机,2 个从机,当 1 个主机挂掉了,只能从 2 个从机中再次选 1 个主机
- 国不可一日无君,军不可一日无帅
- 手动选老大
- 模拟测试:1 为 master,2 和 3 为slave,当1 挂掉后,2 篡权为 master,3 跟 2
slaveof no one # 2上执行,没有人能让我臣服,那我就是老大
slaveof 192.168.204.142 6379 # 3跟随2号
- 思考:当 1 再次回归,会怎样?
- 2 和 3已经形成新的集群,和 1 没有任何的关系了,所以 1 成为了光杆司令
3.5.4 复制原理
完成上面几个步骤后就完成了从服务器数据初始化的所有操作,从服务器此时可以接收来自用户的读请求
-
全量复制:Slave 初始化阶段,这时 Slave 需要将 Master 上的所有数据都复制一份 slave 接收到数据文件后,存盘,并加载到内存中(步骤 1234)
-
增量复制:Slave 初始化后,开始正常工作时主服务器发生的写操作同步到从服务器的过程(步骤 56)
- 但,只要是重新连接 master,一次性(全量复制)同步将自动执行
-
Redis 主从同步策略:主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步
-
当然,如果有需要,slave 在任何时候都可以发起全量同步
-
redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步(shutdown再重新认master)
3.5.5 哨兵模式
-
自动版的谋权篡位
-
只要发现主机挂了,从机们就会内部投票,选出新的主机
-
Sentinel 是 Redis 的高可用性解决方案:
- 由一个或多个 Sentinel 实例组成的 Sentinel 系统可以监视任意多个主服务器,以及所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求
-
模拟测试(1主,2和3从)
-
每一台服务器中创建一个配置文件 sentinel.conf,名字绝不能错,并编辑 sentinel.conf
cd /usr/local/bin vi sentinel.conf # sentinel monitor 被监控主机名(自定义) ip port 票数 sentinel monitor redis141 192.168.204.141 6379 1 sentinel monitor redis142 192.168.204.142 6379 1 sentinel monitor redis143 192.168.204.143 6379 1
-
启动服务顺序:主 Redis(141) --> 从 Redis(142 143)(执行
slaveof 192.168.204.141 6379
认141为master) --> Sentinel1/2/3# 新开三个窗口 cd /usr/local/bin # 启动哨兵 redis-sentinel sentinel.conf
-
将 1 号老大挂掉,后台自动发起激烈的投票,选出新的老大
127.0.0.1:6379> shutdown not connected> exit
-
查看最后权利的分配
- 3 成为了新的老大,2 还是小弟
-
如果之前的老大再次归来呢?
- 1 号再次归来,自己成为了 master,和 3 平起平坐
- 过了几秒之后,被哨兵检测到了 1 号机的归来,1 号你别自己玩了,进入集体吧,但是新的老大已经产生了,你只能作为小弟再次进入集体!
-
3.5.6 缺点
- 由于所有的写操作都是在 master 上完成的,然后再同步到 slave 上,所以两台机器之间通信会有延迟
- 当系统很繁忙的时候,延迟问题会加重
- slave 机器数量增加,问题也会加重
3.6 Jedis
Java 和 Redis 打交道的 API 客户端
Java => MySQL:JDBC
Java => Redis:Jedis
导入依赖:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.1.0</version>
</dependency>
3.6.1 连接 redis
private void Test1(){
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.204.141",6379);
String pong = jedis.ping();
System.out.println("pong = " + pong);
}
}
// 运行前:
// 1.关闭防火墙 systemctl stop firewalld.service
// 2.修改redis.conf [ bind 0.0.0.0 ] 允许任何ip访问,以这个redis.conf启动redis服务
// 3.重启redis: redis-server /opt/redis-5.0.4/redis.conf
3.6.2 常用API
public class Test2_API {
private void testString(){
Jedis jedis = new Jedis("192.168.204.141",6379);
// string
jedis.set("k1","v1");
jedis.set("k2","v2");
jedis.set("k3","v3");
Set<String> set = jedis.keys("*");
Iterator<String> iterator = set.iterator();
for (set.iterator();iterator.hasNext();){
String k = iterator.next();
System.out.println(k+"->"+jedis.get(k));
}
Boolean k2Exists = jedis.exists("k2"); // 查看k2是否存在
System.out.println("k2Exists = " + k2Exists);
System.out.println( jedis.ttl("k1") );// 查看k1的过期时间
jedis.mset("k4","v4","k5","v5");
System.out.println( jedis.mget("k1","k2","k3","k4","k5") );
System.out.println("--------------------------------------------------------");
}
private void testList(){
Jedis jedis = new Jedis("192.168.204.141",6379);
// list
jedis.lpush("list01", "l1","l2","l3","l4","l5");
List<String> list01 = jedis.lrange("list01", 0, -1);
for(String s : list01){
System.out.println(s);
}
System.out.println("--------------------------------------------------------");
}
private void testSet(){
Jedis jedis = new Jedis("192.168.204.141",6379);
// set
jedis.sadd("order","jd001");
jedis.sadd("order","jd002");
jedis.sadd("order","jd003");
Set<String> order = jedis.smembers("order");
Iterator<String> order_iterator = order.iterator();
while(order_iterator.hasNext()){
String s = order_iterator.next();
System.out.println(s);
}
jedis.srem("order", "jd002");
System.out.println( jedis.smembers("order").size() );
}
private void testHash(){
Jedis jedis = new Jedis("192.168.204.141",6379);
jedis.hset("user1", "username","james");
System.out.println( jedis.hget("user1", "username") );
HashMap<String, String> map = new HashMap<String, String>();
map.put("username", "tom");
map.put("gender", "boy");
map.put("address", "beijing");
map.put("phone", "13590875543");
jedis.hmset("user2", map);
List<String> list = jedis.hmget("user2", "username", "phone");
for(String s: list){
System.out.println(s);
}
}
private void testZset(){
Jedis jedis = new Jedis("192.168.204.141",6379);
jedis.zadd("zset01", 60d, "zs1");
jedis.zadd("zset01", 70d, "zs2");
jedis.zadd("zset01", 80d, "zs3");
jedis.zadd("zset01", 90d, "zs4");
Set<String> zset01 = jedis.zrange("zset01", 0, -1);
Iterator<String> iterator = zset01.iterator();
while (iterator.hasNext()){
String s = iterator.next();
System.out.println(s);
}
}
public static void main(String[] args) {
new Test2_API().testZset();
}
}
3.6.3 事务
初始化余额和支出
127.0.0.1:6379> set yue 100
127.0.0.1:6379> set zhichu 0
public class Test_Transaction {
public static void main(String[] args) throws Exception {
Jedis jedis = new Jedis("192.168.204.141",6379);
int yue = Integer.parseInt( jedis.get("yue") );
int zhichu = 10;
jedis.watch("yue"); // 监控余额
Thread.sleep(5000); // 模拟网络延迟
if(yue < zhichu) {
jedis.unwatch(); //解除监控
System.out.println("余额不足!");
}else {
Transaction transaction = jedis.multi(); // 开启事务
transaction.decrBy("yue", zhichu); // 余额减少
transaction.incrBy("zhichu", zhichu); // 累计消费增加
transaction.exec();
System.out.println("余额:" + jedis.get("yue"));
System.out.println("累计支出:" + jedis.get("zhichu"));
}
}
}
- 模拟网络延迟:10 秒内,进入 linux 修改余额为 5,这样,余额<支出,就会进入 if
3.6.4 JedisPool
- redis 的连接池技术
- JedisPool:水池;Jedis:水池里的鱼
- 详情:https://help.aliyun.com/document_detail/98726.html
<dependency> <groupId>commons-pool</groupId> <artifactId>commons-pool</artifactId> <version>1.6</version></dependency>
- 使用单例模式进行优化
public class JedisPoolUtil {
private JedisPoolUtil(){}
private volatile static JedisPool jedisPool = null; // 相当于水池
private volatile static Jedis jedis = null; // 相当于水池里的鱼
// 返回一个连接池 本类调用
private static JedisPool getInstance() {
// 双层检测锁(企业中用的非常频繁)
if(jedisPool == null) { // 第一层:检测体温
synchronized (JedisPoolUtil.class) { // 排队进站
if(jedisPool == null) { //第二层:查看健康码
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(1000); // 资源池中的最大连接数
config.setMaxIdle(30); // 资源池允许的最大空闲连接数
config.setMaxWaitMillis(60*1000); // 当资源池连接用尽后,调用者的最大等待时间(单位为毫秒)
config.setTestOnBorrow(true); //向资源池借用连接时是否做连接有效性检测(业务量很大时候建议设置为false,减少一次ping的开销)
jedisPool = new JedisPool( config, "192.168.204.141", 6379);
}
}
}
return jedisPool;
}
// 返回jedis对象 提供给所有人使用
public static Jedis getJedis() {
if(jedis == null) {
jedis = getInstance().getResource();
}
return jedis;
}
}
- 测试类
public class Test_JedisPool {
public static void main(String[] args) {
Jedis jedis1 = JedisPoolUtil.getJedis();
Jedis jedis2 = JedisPoolUtil.getJedis();
System.out.println(jedis1==jedis2); // true
}
}
3.7 高并发下的分布式锁
- 经典案例:秒杀,抢购优惠券等
3.7.1 搭建工程并测试单线程
1)依赖准备
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.7.RELEASE</version>
</dependency>
<!--实现分布式锁的工具类-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.1</version>
</dependency>
<!--spring操作redis的工具类-->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>2.3.2.RELEASE</version>
</dependency>
<!--redis客户端-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.1.0</version>
</dependency>
<!--json解析工具-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.8</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<configuration>
<port>8001</port>
<path>/</path>
</configuration>
<executions>
<execution>
<!-- 打包完成后,运行服务 -->
<phase>package</phase>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
2)web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
id="WebApp_ID" version="3.1">
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/spring.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
3)spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="controller"/>
<bean id="stringRedisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
<property name="connectionFactory" ref="connectionFactory"></property>
</bean>
<bean id="connectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property name="hostName" value="192.168.204.141"></property>
<property name="port" value="6379"/>
</bean>
</beans>
4)redis
cd /usr/local/bin
redis-server /opt/redis-5.0.4/redis.conf
redis-cli
127.0.0.1:6379> set phone 10
5)controller
@Controller
public class TestKill {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("kill")
// 只能解决一个tomcat的并发问题:synchronized锁的一个进程下的线程并发,如果分布式环境,多个进程并发,这种方案就失效了!
public @ResponseBody synchronized String kill() {
// 1.从redis中获取 手机的库存数量
int phoneCount = Integer.parseInt(stringRedisTemplate.opsForValue().get("phone"));
// 2.判断手机的数量是否够秒杀的
if(phoneCount > 0) {
phoneCount--;
// 库存减少后,再将库存的值保存回redis
stringRedisTemplate.opsForValue().set("phone", phoneCount + "");
System.out.println("库存-1,剩余:"+ phoneCount);
}else {
System.out.println("库存不足!");
}
return "over!";
}
}
6)JMeter测试
3.7.2 高并发测试
- 启动两次工程,端口号分别 8001 和 8002
- 使用 nginx 做负载均衡
vi /usr/local/nginx/conf/nginx.conf
upstream sga{
#本机IP地址 不是虚拟机的
server 192.168.204.1:8001;
server 192.168.204.1:8002;
}
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
proxy_pass http://sga;
root html;
index index.html index.htm;
}
}
# 启动nginx -c: 以nginx.conf配置文件启动
/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
# 关闭防火墙
systemctl stop firewalld.service
-
使用 JMeter 模拟 1 秒内发出 100 个 http 请求,会发现同一个商品会被两台服务器同时抢购!
- 访问nginx,所以JMeter测试用例中IP地址改为虚拟机地址、端口号改为nginx.conf中的80
-
synchronized锁的一个进程下的线程并发,如果分布式环境,多个进程并发,这种方案就失效了
3.7.3 实现分布式锁的思路
- 因为 redis 是单线程的,所以命令也就具备原子性,使用 setnx 命令实现锁,保存 k-v
- 如果 k 不存在,保存(当前线程加锁),执行完成后,删除 k 表示释放锁
- 如果 k 已存在,阻塞线程执行,表示有锁
- 如果加锁成功,在执行业务代码的过程中出现异常,导致没有删除 k(释放锁失败),那么就会造成死锁(后面的所有线程都无法执行)
- 设置过期时间,例如 10 秒后,redis 自动删除
- 高并发下,由于时间段等因素导致服务器压力过大或过小,每个线程执行的时间不同
- 第一个线程,执行需要 13 秒,执行到第 10 秒时,redis 自动过期了 k(释放锁)
- 第二个线程,执行需要 7 秒,加锁,执行第 3 秒(锁被释放了,为什么,是被第一个线程的 finally 主动 deleteKey 释放掉了)
- 连锁反应,当前线程刚加的锁,就被其他线程释放掉了,周而复始,导致锁会永久失效
- 给每个线程加上唯一的标识 UUID 随机生成,释放的时候判断是否是当前的标识即可
- 问题又来了,过期时间如果设定?
- 如果 10 秒太短不够用怎么办?
- 设置 60 秒,太长又浪费时间
- 可以开启一个定时器线程,当过期时间小于总过期时间的 1/3 时,增长总过期时间
自己实现分布式锁,太难了!
3.7.4 Redisson
- Redis 是最流行的 NoSQL 数据库解决方案之一,而 Java 是世界上最流行的编程语言之一
- 虽然两者看起来很自然地在一起“工作”,但是要知道,Redis 其实并没有对 Java 提供原生支持
- 相反,作为 Java 开发人员,我们若想在程序中集成 Redis,必须使用 Redis 的第三方库
- 而 Redisson 就是用于在 Java 程序中操作 Redis 的库,它使得我们可以在程序中轻松地使用 Redis
- Redisson 在 java.util 中常用接口的基础上,为我们提供了一系列具有分布式特性的工具类
@Controller
public class TestKill {
@Autowired
private Redisson redisson;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("kill")
// 只能解决一个tomcat的并发问题:synchronized锁的一个进程下的线程并发,如果分布式环境,多个进程并发,这种方案就失效了!
public @ResponseBody synchronized String kill() {
// 定义商品id
String productKey = "HUAWEI-P40";
// 通过redisson获取锁
RLock rLock = redisson.getLock(productKey); // 底层源码就是集成了setnx,过期时间等操作
// 上锁(过期时间为30秒)
rLock.lock(30, TimeUnit.SECONDS);
try{
// 1.从redis中获取 手机的库存数量
int phoneCount = Integer.parseInt(stringRedisTemplate.opsForValue().get("phone"));
// 2.判断手机的数量是否够秒杀的
if (phoneCount > 0) {
phoneCount--;
// 库存减少后,再将库存的值保存回redis
stringRedisTemplate.opsForValue().set("phone", phoneCount + "");
System.out.println("库存-1,剩余:" + phoneCount);
} else {
System.out.println("库存不足!");
}
} catch(Exception e) {
e.printStackTrace();
}finally {
// 释放锁
rLock.unlock();
}
return "over!";
}
@Bean
public Redisson redisson(){
Config config = new Config();
// 使用单个redis服务器
config.useSingleServer().setAddress("redis://192.168.204.141:6379").setDatabase(0);
// 使用集群redis
// config.useClusterServers().setScanInterval(2000).addNodeAddress("redis://192.168.204.141:6379","redis://192.168.204.142:6379","redis://192.168.204.143:6379");
return (Redisson)Redisson.create(config);
}
}
-
实现分布式锁的方案其实有很多,我们之前用过的 zookeeper 的特点就是高可靠性,现在我们用的 redis 特点就是高性能
-
目前分布式锁,应用最多的仍然是 “Redis”