Redis

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):分区容错性
      • 即分布式系统在遇到某节点或网络分区故障时,仍然能够对外提供满足一致性或可用性的服务。
      • 分区容错性要求能够使应用虽然是一个分布式系统,而看上去却好像是在一个可以运转正常的整体。比如现在的分布式系统中有某一个或者几个机器宕掉了,其他剩下的机器还能够正常运转满足系统需求,对于用户而言并没有什么体验上的影响。
1.4.2 CAP 理论
  • CAP 理论提出就是针对分布式数据库环境的,所以,P 这个属性必须容忍它的存在,而且是必须具备的

  • 因为 P 是必须的,那么我们需要选择的就是 A 和 C

  • 在分布式环境下,为了保证系统可用性,通常都采取了复制的方式,避免一个节点损坏,导致系统不可用。那么就出现了每个节点上的数据出现了很多个副本的情况,而数据从一个节点复制到另外的节点时需要时间和要求网络畅通的,所以,当 P 发生时,也就是无法向某个节点复制数据时,这时候你有两个选择:

    • 选择可用性 A,此时,那个失去联系的节点依然可以向系统提供服务,不过它的数据就不能保证是同步的了(失去了 C 属性)
    • 选择一致性 C,为了保证数据库的一致性,我们必须等待失去联系的节点恢复过来,在这个过程中,那个节点是不允许对外提供服务的,这时候系统处于不可用状态(失去了 A 属性)
  • 最常见的例子是读写分离,某个节点负责写入数据,然后将数据同步到其它节点,其它节点提供读取的服务,当两个节点出现通信问题时,你就面临着选择A(继续提供服务,但是数据不保证准确),C(用户处于等待状态,一直等到数据同步完成)

1.4.3 CAP 总结
  1. 分区是常态,不可避免,三者不可共存
  2. 可用性和一致性是一对冤家
    • 一致性高,可用性低
    • 一致性低,可用性高
  3. 因此,根据 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 上

  1. 上传 tar.gz 包,并解压

    cd /opt
    tar -zxvf redis-5.0.4.tar.gz
    
  2. 安装 gcc(必须有网络)

    yum -y install gcc
    
  3. 进入 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 测试性能
  1. 先 ctrl+c,退出 redis 客户端
  2. 执行 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 不存在
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
  1. 默认的自动备份策略不利于我们测试,所以修改 redis.conf 文件中的自动备份策略

    • 当然如果你只是用 Redis 的缓存功能,不需要持久化,那么你可以注释掉所有的 save 行来停用保存功能,可以直接一个空字符串来实现停用:save “”
    vim redis.conf
    /SNAP   # 搜索
    
    save 900 1      # 900秒内,至少变更1次,才会自动备份
    save 120 10     # 120秒内,至少变更10次,才会自动备份
    save 60 10000   # 60秒内,至少变更10000次,才会自动备份
    
  2. 使用 shutdown 模拟关机 ,关机之前和关机之后,对比 dump.rdb 文件的更新时间

    • 注意:当我们使用 shutdown 命令,Redis 会自动将数据库备份,所以,dump.rdb 文件创建时间更新了
  3. 开机启动 Redis,我们要在 120 秒内保存 10 条数据,再查看 dump.rdb 文件的更新时间(开两个终端窗口,方便查看)

  4. 120 秒内保存 10 条数据这一动作触发了备份指令,目前,dump.rdb 文件中保存了 10 条数据,将 dump.rdb 拷贝一份 dump10.rdb,此时两个文件中都保存 10 条数据

  5. 既然有数据已经备份了,那我们就肆无忌惮的将数据全部删除 flushall,再次 shutdown 关机

  6. 再次启动 Redis,发现数据真的消失了,并没有按照我们所想的将 dump.rdb 文件中的内容恢复到 Redis中。为什么?

    • 因为,当我们保存 10 条以上的数据时,数据备份起来了;然后删除数据库,备份文件中的数据,也没问题;
    • 但是,问题出在 shutdown 上,**这个命令一旦执行,就会立刻备份,将删除之后的空数据库生成备份文件,将之前装 10 条数据的备份文件覆盖掉了。**所以,就出现了上图的结果。自动恢复失败。
    • 怎么解决这个问题呢?要将备份文件再备份。
  7. 将 dump.rdb 文件删除,将 dump10.rdb 重命名为 dump.rdb

  8. 启动 Redis 服务,登录 Redis,数据 10 条,全部恢复!

3.2.1.2 手动备份
  • 之前自动备份,必须更改好多数据,例如上边,我们改变了十多条数据,才会自动备份
  • 现在,我只保存一条数据,就想立刻备份,应该怎么做?
  • 每次操作完成,执行命令 save 就会立刻备份
3.2.1.3 与 RDB 相关的配置
  1. stop-writes-on-bgsave-error:进水口和出水口,出水口发生故障与否
    • yes:当后台备份时候反生错误,前台停止写入
    • no:不管死活,就是往里怼
  2. rdbcompression:对于存储到磁盘中的快照,是否启动 LZF 压缩算法,一般都会启动,因为这点性能,多买一台电脑,完全搞定 N 个来回了。
    • yes:启动
    • no:不启动(不想消耗CPU资源,可关闭)
  3. rdbchecksum:在存储快照后,是否启动 CRC64 算法进行数据校验;
    • 开启后,大约增加 10% 左右的 CPU 消耗;
    • 如果希望获得最大的性能提升,可以选择关闭;
  4. dbfilename:快照备份文件名字
  5. dir:快照备份文件保存的目录,默认为当前目录

优势 / 劣势

  • 优:适合大规模数据恢复,对数据完整性和一致行要求不高;
  • 劣:一定间隔备份一次,意外 down 掉,就失去最后一次快照的所有修改
3.2.2 AOF(Append Only File)
  • 日志的形式记录每个写操作
  • 将 Redis 执行过的写指令全部记录下来(读操作不记录)
  • 只许追加文件,不可以改写文件
  • Redis 在启动之初会读取该文件从头到尾执行一遍,这样来重新构建数据
3.2.2.1 开启AOF
  1. 为了避免失误,最好将 redis.conf 总配置文件备份一下,然后再修改内容如下:

    appendonly  yes
    appendfilename  appendonly.aof
    
  2. 重新启动 redis,以新配置文件启动

    redis-server /usr/local/redis5.0.4/redis.conf
    
  3. 连接 redis,加数据,删库,退出

  4. 查看当前文件夹多一个 aof 文件,看看文件中的内容,保存的都是写操作

    • 文件中最后一句要删除,否则数据恢复不了
    • 编辑这个文件,最后要 :wq! 强制执行
  5. 只需要重新连接,数据恢复成功

3.2.2.2 共存?谁优先?

我们查看 redis.conf 文件,AOF 和 RDB 两种备份策略可以同时开启,那系统会怎样选择?

  1. 动手试试,编辑 appendonly.aof,胡搞乱码,保存退出

  2. 启动 redis 失败(写入代码格式不对),所以是 AOF 优先载入来恢复原始数据!因为 AOF 比 RDB 数据保存的完整性更高!

  3. 修复 AOF 文件,杀光不符合 redis 语法规范的代码

    reids-check-aof  --fix appendonly.aof
    
3.2.2.3 与 AOF 相关的配置
  1. appendonly:开启 AOF 模式
  2. appendfilename:aof 的文件名字,最好别改!
  3. appendfsync:追写策略
    • always:每次数据变更,就会立即记录到磁盘,性能较差,但数据完整性好
    • everysec:默认设置,异步操作,每秒记录,如果一秒内宕机,会有数据丢失
    • no:不追写
  4. no-appendfsync-on-rewrite:重写时是否运用 Appendfsync 追写策略;用默认 no 即可,保证数据安全性
    • AOF 采用文件追加的方式,文件会越来越大,为了解决这个问题,增加了重写机制,redis 会自动记录上一次 AOF 文件的大小,当 AOF 文件大小达到预先设定的大小时,redis 就会启动 AOF 文件进行内容压缩,只保留可以恢复数据的最小指令集合
  5. auto-aof-rewrite-percentage:如果 AOF 文件大小已经超过原来的 100%,也就是一倍,才重写压缩
  6. 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 一主二从
  1. 准备三台服务器,并修改 redis.conf

    bind 0.0.0.0 #允许任何ip访问 重启redis: redis-server /opt/redis5.0.4/redis.conf
    
  2. 启动三台 redis,并查看每台机器的角色,都是 master

    info replication
    
  3. 测试开始

    1. 首先,将三个机器全都清空,第一台添加值

      mset k1 v1 k2 v2
      
    2. 其余两台机器,复制(找大哥)

      slaveof 192.168.204.141 6379
      
    3. 第一台再添加值

      set k3 v3
      
  4. 思考:slave 之前的 k1 和 k2 是否能拿到?

    • 可以获得,只要跟了大哥,之前的数据也会立刻同步
  5. 思考:slave 之后的 k3 是否能拿到?

    • 可以获得,只要跟了大哥,数据会立刻同步
  6. 思考:同时添加 k4,结果如何?

    • 主机(master)可以添加成功,从机(slave)失败,从机只负责读取数据,无权写入数据,这就是**“读写分离”**
  7. 思考:主机 shutdown,从机如何?

    • 142 和 143 仍然是 slave,并显示他们的 master 已离线
  8. 思考:主机重启,从机又如何?

    • 142 和 143 仍然是 slave,并显示他们的 master 已上线
  9. 思考:从机死了,主机如何?从机归来身份是否变化?

    • 主机没有变化,只是显示少了一个 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从)

    1. 每一台服务器中创建一个配置文件 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
      
    2. 启动服务顺序:主 Redis(141) --> 从 Redis(142 143)(执行slaveof 192.168.204.141 6379认141为master) --> Sentinel1/2/3

      # 新开三个窗口
      cd /usr/local/bin
      # 启动哨兵
      redis-sentinel sentinel.conf 
      
    3. 将 1 号老大挂掉,后台自动发起激烈的投票,选出新的老大

      127.0.0.1:6379> shutdown
      not connected> exit
      
    4. 查看最后权利的分配

      • 3 成为了新的老大,2 还是小弟
    5. 如果之前的老大再次归来呢?

      • 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 高并发测试
  1. 启动两次工程,端口号分别 8001 和 8002
  2. 使用 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
  1. 使用 JMeter 模拟 1 秒内发出 100 个 http 请求,会发现同一个商品会被两台服务器同时抢购!

    • 访问nginx,所以JMeter测试用例中IP地址改为虚拟机地址、端口号改为nginx.conf中的80
  2. synchronized锁的一个进程下的线程并发,如果分布式环境,多个进程并发,这种方案就失效了

在这里插入图片描述
在这里插入图片描述

3.7.3 实现分布式锁的思路
  1. 因为 redis 是单线程的,所以命令也就具备原子性,使用 setnx 命令实现锁,保存 k-v
    • 如果 k 不存在,保存(当前线程加锁),执行完成后,删除 k 表示释放锁
    • 如果 k 已存在,阻塞线程执行,表示有锁
  2. 如果加锁成功,在执行业务代码的过程中出现异常,导致没有删除 k(释放锁失败),那么就会造成死锁(后面的所有线程都无法执行)
    • 设置过期时间,例如 10 秒后,redis 自动删除
  3. 高并发下,由于时间段等因素导致服务器压力过大或过小,每个线程执行的时间不同
    • 第一个线程,执行需要 13 秒,执行到第 10 秒时,redis 自动过期了 k(释放锁)
    • 第二个线程,执行需要 7 秒,加锁,执行第 3 秒(锁被释放了,为什么,是被第一个线程的 finally 主动 deleteKey 释放掉了)
    • 连锁反应,当前线程刚加的锁,就被其他线程释放掉了,周而复始,导致锁会永久失效
  4. 给每个线程加上唯一的标识 UUID 随机生成,释放的时候判断是否是当前的标识即可
  5. 问题又来了,过期时间如果设定?
    • 如果 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”

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值