Redis&FastDFS&RabbitMQ

Redis

互联网架构的演变历程

第1阶段
  • 数据访问量不大,简单的架构即可搞定!
第2阶段
  • 数据访问量大,使用缓存技术来缓解数据库的压力。
  • 不同的业务访问不同的数据库

第3阶段

  • 主从读写分离。
  • 之前的缓存确实能够缓解数据库的压力,但是写和读都集中在一个数据库上,压力又来了。
  • 一个数据库负责写,一个数据库负责读。分工合作。愉快!
  • 让master(主数据库)来响应事务性(增删改)操作,让slave(从数据库)来响应非事务性(查询)操作,然后再采用主从复制来把master上的事务性操作同步到slave数据库中
  • mysql的master/slave就是网站的标配!
 
第4阶段
  • mysql的主从复制,读写分离的基础上,mysql的主库开始出现瓶颈
  • 由于MyISAM使用表锁,所以并发性能特别差
  • 分库分表开始流行,mysql也提出了表分区,虽然不稳定,但我们看到了希望
  • 开始吧,mysql集群
 

Redis入门介绍

  • 互联网需求的3高
    • 高并发,高可扩,高性能
  • Redis 是一种运行速度很快,并发性能很强,并且运行在内存上的NoSql(not only sql)数据库
  • NoSQL数据库 和 传统数据库 相比的优势
    • NoSQL数据库无需事先为要存储的数据建立字段,随时可以存储自定义的数据格式。
    • 而在关系数据库里,增删字段是一件非常麻烦的事情。如果是非常大数据量的表,增加字段简直就是一个噩梦

Redis的常用使用场景

  • 缓存,毫无疑问这是Redis当今最为人熟知的使用场景。在提升服务器性能方面非常有效;一些频繁被访问的数据,经常被访问的数据如果放在关系型数据库,每次查询的开销都会很大,而放在redis中,因为redis 是放在内存中的可以很高效的访问
  • 排行榜,在使用传统的关系型数据库(mysql oracle 等)来做这个事儿,非常的麻烦,而利用Redis的SortSet(有序集合)数据结构能够简单的搞定;
  • 计算器/限速器,利用Redis中原子性的自增操作,我们可以统计类似用户点赞数、用户访问数等,这类操作如果用MySQL,频繁的读写会带来相当大的压力;限速器比较典型的使用场景是限制某个用户访问某个API的频率,常用的有抢购时,防止用户疯狂点击带来不必要的压力;
  • 好友关系,利用集合的一些命令,比如求交集、并集、差集等。可以方便搞定一些共同好友、共同爱好之类的功能;
  • 简单消息队列,除了Redis自身的发布/订阅模式,我们也可以利用List来实现一个队列机制,比如:到货通知、邮件发送之类的需求,不需要高可靠,但是会带来非常大的DB压力,完全可以用List来完成异步解耦;
  • Session共享,以jsp为例,默认Session是保存在服务器的文件中,如果是集群服务,同一个用户过来可能落在不同机器上,这就会导致用户频繁登陆;采用Redis保存Session后,无论用户落在那台机器上都能够获取到对应的Session信息。

Redis/Memcache/MongoDB对比

Redis和Memcache

  • Redis和Memcache都是内存数据库。不过memcache还可用于缓存其他东西,例如图片、视频等等。
  • memcache 数据结构单一kv,redis 更丰富一些,还提供 list,set, hash 等数据结构的存储,有效的减少网络 IO 的次数
  • 虚拟内存–Redis当物理内存用完时,可以将一些很久没用到的value交换到磁盘
  • 存储数据安全–memcache挂掉后,数据没了(没有持久化机制);redis可以定期保存到磁盘(持久化)
  • 灾难恢复–memcache挂掉后,数据不可恢复; redis数据丢失后可以通过RBD或AOF恢复

Redis和MongoDB

  • redis和mongodb并不是竞争关系,更多的是一种协作共存的关系。
  • mongodb本质上还是硬盘数据库,在复杂查询时仍然会有大量的资源消耗,而且在处理复杂逻辑时仍然要不可避免地进行多次查询。
  • 这时就需要redis或Memcache这样的内存数据库来作为中间层进行缓存和加速。
  • 比如在某些复杂页面的场景中,整个页面的内容如果都从mongodb中查询,可能要几十个查询语句,耗时很长。如果需求允许,则可以把整个页面的对象缓存至redis中,定期更新。这样mongodb和redis就能很好地协作起来

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

CAP理论

  • CAP理论提出就是针对分布式数据库环境的,所以,P这个属性必须容忍它的存在,而且是必须具备的。
  • 因为P是必须的,那么我们需要选择的就是A和C。
  • 大家知道,在分布式环境下,为了保证系统可用性,通常都采取了复制的方式,避免一个节点损坏,导致系统不可用。那么就出现了每个节点上的数据出现了很多个副本的情况,而数据从一个节点复制到另外的节点时需要时间和要求网络畅通的,所以,当P发生时,也就是无法向某个节点复制数据时,这时候你有两个选择:
    • 选择可用性 A,此时,那个失去联系的节点依然可以向系统提供服务,不过它的数据就不能保证是同步的了(失去了C属性)。
    • 选择一致性C,为了保证数据库的一致性,我们必须等待失去联系的节点恢复过来,在这个过程中,那个节点是不允许对外提供服务的,这时候系统处于不可用状态(失去了A属性)。
  • 最常见的例子是读写分离,某个节点负责写入数据,然后将数据同步到其它节点,其它节点提供读取的服务,当两个节点出现通信问题时,你就面临着选择A(继续提供服务,但是数据不保证准确),C(用户处于等待状态,一直等到数据同步完成)。

CAP总结

分区是常态,不可避免,三者不可共存
可用性和一致性是一对冤家
一致性高,可用性低
一致性低,可用性高
 因此,根据 CAP 原理将 NoSQL 数据库分成了满足 CA 原则、满足 CP 原则和满足 AP 原则三大类:
CA - 单点集群,满足一致性,可用性的系统,通常在可扩展性上不太强大。
CP - 满足一致性,分区容忍性的系统,通常性能不是特别高。
AP - 满足可用性,分区容忍性的系统,通常可能对一致性要求低一些。

 redis配置及命令详解

 redis默认不会使用后台运行,如果你需要,修改配置文件daemonize=yes,当你后台服务启动的时候,会写成一个进程文件运行。
vim /opt/redis-5.0.4/redis.conf

以配置文件的方式启动

cd /usr/local/bin 
redis-server /opt/redis-5.0.4/redis.conf

关闭数据库

单实例关闭

redis-cli shutdown

多实例关闭

redis-cli -p 6379 shutdown

检测6379端口是否在监听

netstat -lntp | grep 6379

检测后台进程是否存在

ps -ef|grep redis

连接redis并测试

redis-cli

测试性能

redis-benchmark

默认16个数据库

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

数据库键的数量

dbsize
redis在linux支持命令补全(tab)

清空数据库

flushdb
清空所有(16个)库,慎用!!
flushall

模糊查询(key)

模糊查询keys命令,有三个通配符:
*:通配任意多个字符
?:通配单个字符
你只记得第一个字母是k,他的长度是3
keys k??

[]:通配括号内的某一个字符

记得其他字母,就第二个字母可能是a或e
keys r[ae]dis

键(key)

exists key:判断某个key是否存在
127.0.0.1:6379> exists k1 
(integer) 1 # 存在
move key db:移动(剪切,粘贴)键到几号库
127.0.0.1:6379> move x1 8 # 将x1移动到8号库 
(integer) 1 # 移动成功
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字符串

使用Redis

字符串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:添加数据的同时设置生命周期
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自动销毁
set if not exist:添加数据的时候判断是否已经存在,防止已存在的数据被覆盖掉
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
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的值添加到数据库

列表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:截取指定范围的值,别的全扔掉
  • ltrim key begindex endindex
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:改变某个下标的某个值
lset key index value
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:插入元素(指定某个元素之前/之后)
linsert key before/after oldvalue newvalue
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"
性能总结:类似添加火车皮一样,头尾操作效率高,中间操作效率惨;

集合Set

和java中的set特点类似,不允许重复
  • sadd/smembers/sismember:添加/查看/判断是否存在
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 # 不存在
注意:1和0可不是下标,而是布尔。1:true存在,2:false不存在
 
scard:获得集合中的元素个数
127.0.0.1:6379> scard set01
 (integer) 3 # 集合中有3个元素
srem:删除集合中的元素
  • srem key value
127.0.0.1:6379> srem set01 2 # 移除set01中的元素2
 (integer) 1 # 1表示移除成功
srandmember:从集合中随机获取几个元素
  • 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"
spop:随机出栈(移除)
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> spop set01 # 随机移除一个元素 
"8"
smove:移动元素:将key1某个值赋值给key2
127.0.0.1:6379> sadd set01 1 2 3 4 5 
(integer) 5 
127.0.0.1:6379> sadd set02 x y z 
(integer) 3 
127.0.0.1:6379> smove set01 set02 3 # 将set01中的元素3移动到set02中 
(integer) 1 # 移动成功
数学集合类
  • 交集: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 setr01 set02 
(empty list or set) 
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"

哈希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 student
1) "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" # 可以添加中文,但是显示为乱码(后期解决)

有序集合Zset

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:模糊查询
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~40之间,共有几个元素
(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)

持久化

RDB
Redis DataBase
在指定的时间间隔内,将内存中的数据集的快照写入磁盘;
默认保存在/usr/local/bin中,文件名dump.rdb; 

自动备份

  • redis是内存数据库,当我们每次用完redis,关闭linux时,按道理来说,内存释放,redis中的数据也会随之消失
  • 为什么我们再次启动redis的时候,昨天的数据还在,并没有消失呢?
  • 正是因为,每次关机时,redis会自动将数据备份到一个文件中 :/usr/local/bin/dump.rdb
  • 接下来我们就来全方位的认识 自动备份机制
 1. 默认的自动备份策略不利于我们测试,所以修改redis.conf文件中的自动备份策略
vim redis.conf
/SNAP # 搜索
save 900 1 # 900秒内,至少变更1次,才会自动备份
save 120 10 # 120秒内,至少变更10次,才会自动备份
save 60 10000 # 60秒内,至少变更10000次,才会自动备份

当然如果你只是用Redis的缓存功能,不需要持久化,那么你可以注释掉所有的 save 行来停用保存功能。可以直接一个空字符串来实现停用:save ""

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条,全部恢复!

手动备份

  • 之前自动备份,必须更改好多数据,例如上边,我们改变了十多条数据,才会自动备份;
  • 现在,我只保存一条数据,就想立刻备份,应该怎么做?
  • 每次操作完成,执行命令 save 就会立刻备份 
RDB相关的配置
stop-writes-on-bgsave-error:进水口和出水口,出水口发生故障与否
  • yes:当后台备份时候反生错误,前台停止写入
  • no:不管死活,就是往里怼
rdbcompression:对于存储到磁盘中的快照,是否启动LZF压缩算法,一般都会启动,因为这点性能,多买一台电脑,完全搞定N个来回了。
  • yes:启动
  • no:不启动(不想消耗CPU资源,可关闭)
rdbchecksum:在存储快照后,是否启动CRC64算法进行数据校验;
  • 开启后,大约增加10%左右的CPU消耗;
  • 如果希望获得最大的性能提升,可以选择关闭;
dbfilename:快照备份文件名字
dir:快照备份文件保存的目录,默认为当前目录
AOF
Append Only File
  • 以日志的形式记录每个写操作;
  • redis执行过的写指令全部记录下来(读操作不记录);
  • 只许追加文件,不可以改写文件;
  • redis在启动之初会读取该文件从头到尾执行一遍,这样来重新构建数据; 
开启AOF
为了避免失误,最好将redis.conf总配置文件备份一下,然后再修改内容如下: 
appendonly yes
appendfilename appendonly.aof
2. 重新启动redis,以新配置文件启动
redis-server /usr/local/redis5.0.4/redis.conf
3. 连接redis,加数据,删库,退出
4. 查看当前文件夹多一个aof文件,看看文件中的内容,保存的都是 操作
文件中最后一句要删除,否则数据恢复不了
编辑这个文件,最后要 :wq! 强制执行
5. 只需要重新连接,数据恢复成功

RDB和AOF共存,哪个优先实现数据备份,实现数据持久化

我们查看redis.conf文件,AOFRDB两种备份策略可以同时开启,那系统会怎样选择?
1. 动手试试,编辑appendonly.aof,胡搞乱码,保存退出
2. 启动redis 失败,所以是AOF优先载入来恢复原始数据!因为AOFRDB数据保存的完整性更高!
3. 修复AOF文件,杀光不符合redis语法规范的代码 
reids-check-aof --fix appendonly.aof
AOF相关的配置
AOF采用文件追加的方式,文件会越来越大,为了解决这个问题,增加了重写机制,redis会自动记录上一次AOF文件的大小,当AOF文件大小达到预先设定的大小时,redis就会启动AOF文件进行内容压缩,只保留可以恢复数据的最小指令集合
总结(如何选择?)
RDB:只用作后备用途,建议15分钟备份一次就好
AOF:
在最恶劣的情况下,也只丢失不超过2秒的数据,数据完整性比较高,但代价太大,会带来持续的IO
对硬盘的大小要求也高,默认64mb太小了,企业级最少都是5G以上;
后面要学习的master/slave才是新浪微博的选择!!

事务

可以一次执行多个命令,是一个命令组,一个事务中,所有命令都会序列化(排队),不会被插队;
一个队列中,一次性,顺序性,排他性的执行一系列命令
三特性:
  隔离性:所有命令都会按照顺序执行,事务在执行的过程中,不会被其他客户端送来的命令打断
  没有隔离级别:队列中的命令没有提交之前都不会被实际的执行,不存在“事务中查询要看到事务里的更新,事务外查询不能看到”这个头疼的问题
  不保证原子性:冤有头债有主,如果一个命令失败,但是别的命令可能会执行成功,没有回滚
三步走:
  开启multi
  入队queued
  执行exec
与关系型数据库事务相比:
  multi:可以理解成关系型事务中的 begin
  exec :可以理解成关系型事务中的 commit
  discard :可以理解成关系型事务中的 rollback

一起生(exec)

开启事务,加入队列,一起执行,并成功
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

一起死(discard)

放弃之前的操作,恢复到原来的值
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" # 还是原来的值

一粒老鼠屎坏一锅汤(一句报错,全部取消)

一句报错,全部取消,恢复到原来的值
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"

冤有头债有主

追究责任,谁的错,找谁去
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"

watch监控

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命令,那么之前加的所有监控自动失效!

Redis的发布订阅

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

主从复制

  • 就是 redis集群的策略
  • 配从(库)不配主(库):小弟可以选择谁是大哥,但大哥没有权利去选择小弟
  • 读写分离:主机写,从机读

一主二仆

1. 准备三台服务器,并修改redis.conf
bind 0.0.0.0
2. 启动三台redis,并查看每台机器的角色,都是master
info replication
3. 测试开始
  1. 首先,将三个机器全都清空,第一台添加值
mset k1 v1 k2 v2
  2. 其余两台机器,复制(找大哥)
slaveof 192.168.204.141 6379
  3. 第一台再添加值
set k3 v3
思考1:slave之前的k1和k2是否能拿到?
可以获得,只要跟了大哥,之前的数据也会立刻同步
思考2:slave之后的k3是否能拿到?
可以获得,只要跟了大哥,数据会立刻同步
思考3:同时添加k4,结果如何?
主机(141master)可以添加成功,从机(142和143是slave)失败,从机只负责读取数据,无权写入数据,这就是“读写分离
思考4:主机shutdown,从机如何?
142和143仍然是slave,并显示他们的master已离线
思考5:主机重启,从机又如何?
142和143仍然是slave,并显示他们的master已上线
思考6:从机死了,主机如何?从机归来身份是否变化?
主机没有变化,只是显示少了一个slave
主机和从机没有变化,而重启归来的从机自立门户成为了master,不和原来的集群在一起了

血脉相传

一个主机理论上可以多个从机,但是这样的话,这个主机会很累
我们可以使用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

谋权篡位

  • 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成为了光杆司令

复制原理

完成上面几个步骤后就完成了从服务器数据初始化的所有操作,从服务器此时可以接收来自用户的读请求

全量复制:Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份slave接收到数据文件后,存盘,并加载到内存中;(步骤1234)

 增量复制:Slave初始化后,开始正常工作时主服务器发生的写操作同步到从服务器的过程;(步骤56)

  但,只要是重新连接master,一次性(全量复制)同步将自动执行;
Redis主从同步策略:主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。

当然,如果有需要,slave 在任何时候都可以发起全量同步。

redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。

哨兵模式

 自动版的谋权篡位!

有个哨兵一直在巡逻,突然发现!!!!!老大挂了,小弟们会自动投票,从众小弟中选出新的老大

Sentinel是Redis的高可用性解决方案:

由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主服务器,以及所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求

 模拟测试

1. 1主,2和3从
2. 每一台服务器中创建一个配置文件sentinel.conf,名字绝不能错,并编辑sentinel.conf

# sentinel monitor 被监控主机名(自定义) ip port 票数
sentinel monitor redis141 192.168.204.141 6379 1

3. 启动服务的顺序:主Redis --> 从Redis --> Sentinel1/2/3

redis-sentinel sentinel.conf

4. 将1号老大挂掉,后台自动发起激烈的投票,选出新的老大

127.0.0.1:6379> shutdown
not connected> exit

5. 查看最后权利的分配
  3成为了新的老大,2还是小弟

6. 如果之前的老大再次归来呢?

  1号再次归来,自己成为了master,和3平起平坐
  过了几秒之后,被哨兵检测到了1号机的归来,1号你别自己玩了,进入集体吧,但是新的老大已经产生了,你只能作为小弟再次进入集体!

  缺点

由于所有的写操作都是在master上完成的;

然后再同步到slave上,所以两台机器之间通信会有延迟;

 当系统很繁忙的时候,延迟问题会加重;

slave机器数量增加,问题也会加重

 总配置redis.conf 详解

 参考:https://www.cnblogs.com/zhf123/p/14522703.html

Jedis

java和redis打交道的API客户端

<dependency>
  <groupId>redis.clients</groupId>
  <artifactId>jedis</artifactId>
  <version>3.1.0</version>
</dependency>

连接redis

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服务
(重启redis)
// redis-server /opt/redis5.0.4/redis.conf

常用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);
        }

    }

事务

初始化余额和支出

set yue 100
set zhichu 0
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"));
 }
}

JedisPool

<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);
                    jedisPool = new JedisPool( config, "192.168.199.128",6379 );
                }
            }
        }
        return jedisPool;
    }

    // 返回jedis对象
    public static Jedis getJedis(){
        if(jedis == null){
            jedis = getInstance().getResource();
        }
        return jedis;
    }
}

高并发下的分布式锁

搭建工程并测试单线程

<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>
                <version>2.2</version>
                <configuration>
                    <port>8002</port>
                    <path>/</path>
                </configuration>
                <executions>
                    <execution>
                        <!-- 打包完成后,运行服务 -->
                        <phase>package</phase>
                        <goals>
                            <goal>run</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
<?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>
<?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>
@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!";
 }
}

高并发测试

upstream sga{
   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;
 }

实现分布式锁的思路

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时,增长总过期时间(吃仙丹续命!)

  自己实现分布式锁,太难了!

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”

 分布式文件系统—FastDFS

什么是文件系统

文件数据是如何存储的??

 

 

 分布式文件系统

分布式:不同的业务模块部署在不同的服务器上或者同一个业务模块分拆多个子业务,部署不同的服务器上。解决高并发的问题;

集群:同一个业务部署在多台服务器上,提高系统的高可用

主流的分布式文件系统

 HDFS

  • (Hadoop Distributed File System)Hadoop 分布式文件系统;
  • 高容错的系统,适合部署到廉价的机器上;
  • 能提供高吞吐量的数据访问,非常适合大规模数据应用;
  • HDFS采用主从结构,一个HDFS是由一个name节点和N个data节点组成;
  • name节点储存元数据,一个文件分割成N份存储在不同的data节点上。

GFS

  • Google File System
  • 可扩展的分布式文件系统,用于大型的,分布式的,对大量数据进行访问的应用;
  • 运行于廉价的普通硬件上,可以提供容错功能;
  • 它可以给大量的用户提供总体性能较高的服务;
  • GFS采用主从结构,一个GFS集群由一个master和大量的chunkserver(分块服务器)组成;
  • 一个文件被分割若干块,分散储存到多个分块server中

FastDFS

  • 专为互联网量身定制,充分考虑了冗余备份、负载均衡、线性扩容等机制,并注重高可用、高性能等指标,使用FastDFS很容易搭建一套高性能的文件服务器集群提供文件上传、下载等服务;HDFS,GFS等都是通用的文件系统,他们的优点是开发体验好,但是系统的复杂度较高,性能也一般;
  • 相比之下,专用的分布式文件系统体验差,但是复杂度低,性能也高,尤其fastDFS特别适合图片,小视频等小文件,因为fastDFS对文件是不分割的,所以没有文件合并的开销;
  • 网络通信用socket,速度快。

工作原理

  • fastDFS包含Tracker Server和Storage Server;
  • 客户端请求Tracker Server进行文件的上传与下载;
  • Tracker Server调度Storage Server最终完成上传与下载。

 

 Tracker (译:追踪者)

  • 作用是负载均衡和调度,它管理着存储服务(Storage Server),可以理解为:“大管家,追踪者,调度员”;
  • Tracker Server可以集群,实现高可用,策略为“轮询”。

Storage (译:仓库; 贮存器)

  • 作用是文件存储,客户端上传的文件最终存储到storage服务器上;
  • storage集群采用分组的方式,同组内的每台服务器是平等关系,数据同步,目的是实现数据备份,从而高可用,而不同组的服务器之间是不通信的;
  • 同组内的每台服务器的存储量不一致的情况下,会选取容量最小的那个,所以同组内的服务器之间软硬件最好保持一致。
  • Storage Server会连接集群中的所有Tracker Server,定时向他们汇报自己的状态,例如:剩余空间,文件同步情况,文件上传下载次数等信息。

上传/下载 原理  

 

 

 

 客户端上传文件后,storage会将文件id返回给客户端

group1/M00/02/11/aJxAeF21O5wAAAAAAAAGaEIOA12345.sh

组名:文件上传后,在storage组的名称,文件上传成功后,由storage返回,需要客户端自行保存。

虚拟磁盘路径

storage配置的虚拟路径,在磁盘选项storage_path对应。

storage_path0对应M00,

storage_path1对应M01,

数据两级目录

storage在虚拟磁盘下自行创建的目录。

文件名

与上传时不同,是用storage根据特定信息生成的,里面包含:storage服务器的ip,创建时间戳,大小,后缀名等信息

FastDFS的上传与下载

安装gcc(编译时需要)

yum install -y gcc gcc-c++

安装libevent(运行时需求)

yum -y install libevent

安装libfastcommon

libfastcommon是FastDFS官方提供的,libfastcommon包含了FastDFS运行所需要的一些基础库。

1. 上传libfastcommon-master.zip 到/opt

安装解压zip包的命令:  yum install -y unzip

解压包:             unzip libfastcommon.zip

 

进入目录:           cd libfastcommon-master

2. 编译

./make.sh

如果:make.sh的权限不够,则需要授权(可执行的权利)

chmod 777 make.sh

3. 安装

./make.sh install

libfastcommon安装好后会在/usr/lib64 目录下生成libfastcommon.so 库文件

4. 拷贝库文件

cd /usr/lib64
cp libfastcommon.so /usr/lib

安装Tracker

下载 FastDFS_v5.05.tar.gz,并上传到 /opt

tar -zxvf FastDFS_v5.05.tar.gz
cd FastDFS
./make.sh
./make.sh install

安装成功将安装目录下的conf下的文件拷贝到/etc/fdfs/下

cp /opt/FastDFS/conf/* /etc/fdfs/

安装后,FastDFS主程序所在位置:

/usr/bin - 可执行文件所在位置。

/etc/fdfs - 配置文件所在位置。

/usr/lib64 - 主程序代码所在位置

/usr/include/fastdfs - 包含的一些插件组所在位置

配置

Tracker配置

vim /etc/fdfs/tracker.conf
#端口号
port=22122 
#基础目录(Tracker运行时会向此目录存储storage的管理数据)(基础目录不存在的话,需要自行创建
mkdir /home/fastdfs)
base_path=/home/fastdfs 

Storage配置

vim /etc/fdfs/storage.conf
#配置组名
group_name=group1
#端口
port=23000
#向tracker心跳间隔(秒)
heart_beat_interval=30
#storage基础目录
#目录不存在,需要自行创建
base_path=/home/fastdfs
#store存放文件的位置(store_path)
#可以理解一个磁盘一个path,多个磁盘,多个store_path
#fdfs_storage目录不存在,需要自行创建
#mkdir /home/fastdfs/fdfs_storage
store_path0=/home/fastdfs/fdfs_storage
#如果有多个挂载磁盘则定义多个store_path,如下
#store_path1=.....  (M01)
#store_path2=.....  (M02)
#配置tracker服务器:IP
tracker_server=10.1.220.247:22122
#如果有多个则配置多个tracker
#tracker_server=10.1.220.x:22122

 启动服务

启动tracker

/usr/bin/fdfs_trackerd /etc/fdfs/tracker.conf restart

启动storage

/usr/bin/fdfs_storaged /etc/fdfs/storage.conf restart

查看所有运行的端口:

netstat -ntlp

搭建 Java工程

pom.xml

<dependencies>
        <!--fastdfs的java客户端-->
        <dependency>
            <groupId>net.oschina.zcx7878</groupId>
            <artifactId>fastdfs-client-java</artifactId>
            <version>1.27.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-io</artifactId>
            <version>1.3.2</version>
        </dependency>
    </dependencies>

创建配置文件

在resources下创建config目录,在config目录下创建 fastdfs-client.properties,内容如下:

##fastdfs-client.properties
fastdfs.connect_timeout_in_seconds = 5
fastdfs.network_timeout_in_seconds = 30
fastdfs.charset = UTF-8
fastdfs.http_anti_steal_token = false
fastdfs.http_secret_key = FastDFS1234567890
fastdfs.http_tracker_http_port = 80
fastdfs.tracker_servers = 192.168.199.128:22122

文件上传

package test;

import org.csource.common.NameValuePair;
import org.csource.fastdfs.*;


/**
 * @BelongsProject: lagou-fastdfs
 * @Author: GuoAn.Sun
 * @CreateTime: 2020-07-28 17:33
 * @Description: 文件上传
 */
public class TestUpload {
    public static void main(String[] args) {
        try {
            // 加载配置文件
            ClientGlobal.initByProperties("config/fastdfs-client.properties");

            // 创建tracker客户端
            TrackerClient trackerClient = new TrackerClient();
            // 通过tracker客户端获取tracker的连接服务并返回
            TrackerServer trackerServer = trackerClient.getConnection();
            // 声明storage服务
            StorageServer storageServer = null;
            // 定义storage客户端
            StorageClient1 client = new StorageClient1(trackerServer, storageServer);

            // 定义文件元信息
            NameValuePair[] list = new NameValuePair[1];
            list[0] = new NameValuePair("fileName","1.jpg");

            String fileID = client.upload_file1("D:\\img\\1.jpg", "jpg", list);
            System.out.println("fileID = " + fileID);
            // group1/M00/00/00/CgHc918f8l6AFYp0AAWICfQnHuk889.jpg
            /*
            group1:一台服务器,就是一个组
            M00: store_path0 ----> /home/fastdfs/fdfs_storage/data
            00/00:两级数据目录
             */
            trackerServer.close();
        }catch(Exception e){
            e.printStackTrace();
        }

    }
}

文件查询

package test;

import org.csource.fastdfs.*;

/**
 * @BelongsProject: lagou-fastdfs
 * @Author: GuoAn.Sun
 * @CreateTime: 2020-07-28 17:46
 * @Description: 文件查询
 */
public class TestQuery {
    public static void main(String[] args) throws Exception {

        // 加载配置文件
        ClientGlobal.initByProperties("config/fastdfs-client.properties");

        // 创建tracker客户端
        TrackerClient trackerClient = new TrackerClient();
        // 通过tracker客户端获取tracker的连接服务并返回
        TrackerServer trackerServer = trackerClient.getConnection();
        // 声明storage服务
        StorageServer storageServer = null;
        // 定义storage客户端
        StorageClient1 client = new StorageClient1(trackerServer, storageServer);

        FileInfo fileInfo = client.query_file_info1("group1/M00/00/00/wKjHgGBL7VeAEQpFAAEQA_GV-O8760.jpg");
        if(fileInfo!=null)
            System.out.println("fileInfo = " + fileInfo);
        else
            System.out.println("查无此文件!");
        trackerServer.close();

    }
}

文件下载

package test;

import org.csource.fastdfs.*;

import java.io.File;
import java.io.FileOutputStream;

/**
 * @BelongsProject: lagou-fastdfs
 * @Author: GuoAn.Sun
 * @CreateTime: 2020-07-28 17:49
 * @Description: 文件下载
 */
public class TestDownload {
    public static void main(String[] args) throws Exception{
        // 加载配置文件
        ClientGlobal.initByProperties("config/fastdfs-client.properties");
        // 创建tracker客户端
        TrackerClient trackerClient = new TrackerClient();
        // 通过tracker客户端获取tracker的连接服务并返回
        TrackerServer trackerServer = trackerClient.getConnection();
        // 声明storage服务
        StorageServer storageServer = null;
        // 定义storage客户端
        StorageClient1 client = new StorageClient1(trackerServer, storageServer);

        byte[] bytes = client.download_file1("group1/M00/00/00/wKjHgGBL7VeAEQpFAAEQA_GV-O8760.jpg");

        // 通过io将字节数组,转换成一个文件
        FileOutputStream fileOutputStream = new FileOutputStream(new File("D:/xxxxxx.jpg"));
        fileOutputStream.write(bytes);
        fileOutputStream.close();
        trackerServer.close();
        System.out.println("下载完毕!");
    }
}

项目实战 - 搭建图片服务器

Nginx模块安装 (Storage)

1. 上传 fastdfs-nginx-module_v1.16.tar.gz 到 /opt
2. 解压nginx模块

tar -zxvf fastdfs-nginx-module_v1.16.tar.gz

3. 修改 config 文件,将文件中的 /usr/local/ 路径改为 /usr/

cd /opt/fastdfs-nginx-module/src
vim config

4. 将 fastdfs-nginx-module/src下的 mod_fastdfs.conf 拷贝至 /etc/fdfs 下

cp mod_fastdfs.conf /etc/fdfs/

5. 修改 /etc/fdfs/mod_fastdfs.conf

vim /etc/fdfs/mod_fastdfs.conf
base_path=/home/fastdfs
tracker_server=10.1.220.247:22122
#(n个tracker配置n行)
#tracker_server=10.1.220.x:22122
#url中包含group名称
url_have_group_name=true   
#指定文件存储路径(上面配置的store路径)
store_path0=/home/fastdfs/fdfs_storage 

6. 将 libfdfsclient.so 拷贝至 /usr/lib 下

cp /usr/lib64/libfdfsclient.so /usr/lib/

7. 创建nginx/client目录

mkdir -p /var/temp/nginx/client

Nginx安装 (Tracker)

1. 将 nginx-1.14.0.tar.gz上传到/opt(安装过nginx,此步省略)

2. 解压:tar -zxvf nginx-1.14.0.tar.gz(安装过nginx,此步省略)

3. 安装依赖库(安装过nginx,此步省略)

yum install pcre
yum install pcre-devel
yum install zlib
yum install zlib-devel
yum install openssl
yum install openssl-devel

4. 进入nginx解压的目录下 cd /opt/nginx-1.14.0

5. 安装

./configure \
--prefix=/usr/local/nginx \
--pid-path=/var/run/nginx/nginx.pid \
--lock-path=/var/lock/nginx.lock \
--error-log-path=/var/log/nginx/error.log \
--http-log-path=/var/log/nginx/access.log \
--with-http_gzip_static_module \
--http-client-body-temp-path=/var/temp/nginx/client \
--http-proxy-temp-path=/var/temp/nginx/proxy \
--http-fastcgi-temp-path=/var/temp/nginx/fastcgi \
--http-uwsgi-temp-path=/var/temp/nginx/uwsgi \
--http-scgi-temp-path=/var/temp/nginx/scgi \
--add-module=/opt/fastdfs-nginx-module/src

注意:上边将临时文件目录指定为 /var/temp/nginx,需要在 /var 下创建 temp 及 nginx 目录:mkdir
/var/temp/nginx

6. 编译:make

7. 安装:make install

8. 拷贝配置文件

cd /opt/FastDFS/conf
cp http.conf mime.types /etc/fdfs/
是否覆盖:yes

9. 修改nginx配置文件

cd /usr/local/nginx/conf/
vim nginx.conf
server {
 listen    80;
 server_name 10.1.220.247;
 #charset koi8-r;
 #access_log logs/host.access.log main;
 location /group1/M00 {
   root  /home/fastdfs/fdfs_storage/data;
   ngx_fastdfs_module;
 }

或者使用下面的配置方式:

创建网络访问存储服务的软连接

在上传文件到FastDFS后,FastDFS会返回group1/M00/00/00/xxxxxxxxxx.xxx。其中group1是卷名,在mod_fastdfs.conf配置文件中已配置了url_have_group_name,以保证URL解析正确。

而其中的M00是FastDFS保存数据时使用的虚拟目录,需要将这个虚拟目录定位到真实数据目录上。

ln -s /var/data/fastdfs-storage/store/data/  /var/data/fastdfs-storage/store/data/M00

修改nginx配置文件

location ~ /group([0-9])/M00 {
    ngx_fastdfs_module;
} 

10. 关闭nginx,并启动nginx

pkill -9 nginx
/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf

11. 访问nginx并查看图片

http://10.1.220.247/group1/M00/00/00/CgHc918f8l6AFYp0AAWICfQnHuk889.jpg

实战

搭建web服务

<packaging>war</packaging>

    <dependencies>
        <!-- 因为有jsp页面,所以引用servlet依赖-->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <scope>provided</scope>
            <version>2.5</version>
        </dependency>
        <!-- 页面提交过来的请求,使用springmvc来处理-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.2.7.RELEASE</version>
        </dependency>
        <!-- java连接fastDFS的客户端工具-->
        <dependency>
            <groupId>net.oschina.zcx7878</groupId>
            <artifactId>fastdfs-client-java</artifactId>
            <version>1.27.0.0</version>
        </dependency>
        <!-- 图片上传到FastDFS需要用的到IO工具-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-io</artifactId>
            <version>1.3.2</version>
        </dependency>
        <!-- 图片保存到web服务器需要用到的IO工具-->
        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>1.3.1</version>
        </dependency>
        <!--用来转换java对象和json字符串,注意,2.7以上版本必须搭配spring5.0以上-->
        <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>
                <version>2.2</version>
                <configuration>
                    <port>8001</port>
                    <path>/</path>
                </configuration>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>run</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

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-mvc.xml</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>springMVC</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

spring-mvc.xml

<!--扫描注解的包-->
    <context:component-scan base-package="controller"/>
    <!--扫描控制器中的注解:@Response-->
    <mvc:annotation-driven/>
    <!--上传文件的解析器(规定上传文件的大小限制)-->
    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <!-- 上传文件最大限制:2GB-->
        <property name="maxUploadSize" value="2048000000"/>
    </bean>

文件实体类

public class FileSystem implements Serializable {
  private String fileId;
  private String filePath;
  private String fileName;
}

添加fastDFS的配置文件

package controller;

import entity.FileSystem;
import org.csource.common.NameValuePair;
import org.csource.fastdfs.*;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;

import java.io.File;
import java.util.UUID;

/**
 * @BelongsProject: lagou-imageServer
 * @Author: GuoAn.Sun
 * @CreateTime: 2020-07-29 14:25
 * @Description: 处理上传文件的控制器
 */
@Controller
public class FileAction {

    /**
     * @param request 多部件表单的请求对象
     * @return 上传文件对象的json对象
     * @throws Exception
     *
     * 上传文件的流程:
     * 1、先把文件保存到web服务器上
     * 2、再从web服务器上将文件 上传 到 FastDFS上
     */

    @RequestMapping("upload")
    //MultipartHttpServletRequest:是httpservletRequest的强化版本,不仅可以装文本信息,还可以装图片文件信息
    public @ResponseBody FileSystem upload(MultipartHttpServletRequest request) throws Exception {
        FileSystem fileSystem = new FileSystem();

        /* 1、把文件保存到web服务器*/

        // 从页面请求中,获取上传的文件对象
        MultipartFile file = request.getFile("fname");
        // 从文件对象中获取 文件的原始名称
        String oldFileName = file.getOriginalFilename();
        // 通过字符串截取的方式,从文件原始名中获取文件的后缀   1.jpg
        String hou = oldFileName.substring(oldFileName.lastIndexOf(".") + 1);
        // 为了避免文件因为同名而覆盖,生成全新的文件名
        String newFileName = UUID.randomUUID().toString() + "." + hou;
        // 创建web服务器保存文件的目录(预先创建好D:/upload目录,否则系统找不到路径,会抛异常)
        File toSaveFile = new File("D:/upload/" + newFileName);
        // 将路径转换成文件
        file.transferTo(toSaveFile);
        // 获取服务器的绝对路径
        String newFilePath = toSaveFile.getAbsolutePath();

        /* 2、把文件从web服务器上传到FastDFS*/

        ClientGlobal.initByProperties("config/fastdfs-client.properties");
        TrackerClient trackerClient = new TrackerClient();
        TrackerServer trackerServer = trackerClient.getConnection();
        StorageServer storageServer = null;
        StorageClient1 client = new StorageClient1(trackerServer, storageServer);

        NameValuePair[] list = new NameValuePair[1];
        list[0] = new NameValuePair("fileName",oldFileName);
        String fileId = client.upload_file1(newFilePath, hou, list);
        trackerServer.close();

        // 封装fileSystem数据对象
        fileSystem.setFileId(fileId);
        fileSystem.setFileName(oldFileName);
        fileSystem.setFilePath(fileId);  //已经上传到FastDFS上,通过fileId来访问图片,所以fileId即为文件路径

        return  fileSystem;
    }

}

RabbitMQ详解

什么是RabbitMQ

MQ(Message Queue)消息队列

消息队列中间件,是分布式系统中的重要组件
主要解决,异步处理,应用解耦,流量削峰等问题
从而实现高性能,高可用,可伸缩和最终一致性的架构
使用较多的消息队列产品:RabbitMQ,RocketMQ,ActiveMQ,ZeroMQ,Kafka等

异步处理

用户注册后,需要发送验证邮箱和手机验证码;
将注册信息写入数据库,发送验证邮件,发送手机,三个步骤全部完成后,返回给客户端

 

 

 应用解耦

  • 场景:订单系统需要通知库存系统
  • 如果库存系统异常,则订单调用库存失败,导致下单失败
    • 原因:订单系统和库存系统耦合度太高

 

 

 订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户,下单成功;

库存系统:订阅下单的消息,获取下单信息,库存系统根据下单信息,再进行库存操作;
假如:下单的时候,库存系统不能正常运行,也不会影响下单,因为下单后,订单系统写入消息队
列就不再关心其他的后续操作了,实现了订单系统和库存系统的应用解耦;
所以说,消息队列是典型的:生产者消费者模型
生产者不断的向消息队列中生产消息,消费者不断的从队列中获取消息
因为消息的生产和消费都是异步的,而且只关心消息的发送和接收,没有业务逻辑的入侵,这样就
实现了生产者和消费者的解耦

流量削峰

抢购,秒杀等业务,针对高并发的场景
因为流量过大,暴增会导致应用挂掉,为解决这个问题,在前端加入消息队列

用户的请求,服务器接收后,首先写入消息队列,如果超过队列的长度,就抛弃,甩一个秒杀结束的页面!
说白了,秒杀成功的就是进入队列的用户;

背景知识介绍

AMQP高级消息队列协议

Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议
协议:数据在传输的过程中必须要遵守的规则
基于此协议的客户端可以与消息中间件传递消息
并不受产品、开发语言等条件的限制

  JMS

Java Message Server,Java消息服务应用程序接口, 一种规范,和JDBC担任的角色类似
是一个Java平台中关于面向消息中间件的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信

二者的联系 

JMS是定义了统一接口,统一消息操作;AMQP通过协议统一数据交互格式
JMS必须是java语言;AMQP只是协议,与语言无关

Erlang语言

Erlang(['ə:læŋ])是一种通用的面向并发的编程语言,它由瑞典电信设备制造商爱立信所辖的CS-Lab开发,目的是创造一种可以应对大规模并发活动的编程语言和运行环境
最初是由爱立信专门为通信应用设计的,比如控制交换机或者变换协议等,因此非常适合构建分布式,实时软并行计算系统
Erlang运行时环境是一个虚拟机,有点像Java的虚拟机,这样代码一经编译,同样可以随处运行

为什么选择RabbitMQ

  • 我们开篇说消息队列产品那么多,为什么偏偏选择RabbitMQ呢?
  • 先看命名:兔子行动非常迅速而且繁殖起来也非常疯狂,所以就把Rabbit用作这个分布式软件的命名(就是这么简单)
  • Erlang开发,AMQP的最佳搭档,安装部署简单,上手门槛低
  • 企业级消息队列,经过大量实践考验的高可靠,大量成功的应用案例,例如阿里、网易等一线大厂都有使用
  • 有强大的WEB管理页面
  • 强大的社区支持,为技术进步提供动力
  • 支持消息持久化、支持消息确认机制、灵活的任务分发机制等,支持功能非常丰富
  • 集群扩展很容易,并且可以通过增加节点实现成倍的性能提升
  • 总结:如果你希望使用一个可靠性高、功能强大、易于管理的消息队列系统那么就选择RabbitMQ,如果你想用一个性能高,但偶尔丢点数据不是很在乎可以使用kafka或者zeroMQ
  • kafka和zeroMQ的性能爆表,绝对可以压RabbitMQ一头!

RabbitMQ各组件功能 

 

 

 

Broker:消息队列服务器实体
Virtual Host:虚拟主机
标识一批交换机、消息队列和相关对象,形成的整体
虚拟主机是共享相同的身份认证和加密环境的独立服务器域
每个vhost本质上就是一个mini版的RabbitMQ服务器,拥有自己的队列、交换器、绑定和权限机制
vhost是AMQP概念的基础,RabbitMQ默认的vhost是 /,必须在链接时指定
Exchange:交换器(路由)
用来接收生产者发送的消息并将这些消息路由给服务器中的队列
Queue:消息队列
用来保存消息直到发送给消费者。
它是消息的容器,也是消息的终点。
一个消息可投入一个或多个队列。
消息一直在队列里面,等待消费者连接到这个队列将其取走。
Banding:绑定,用于消息队列和交换机之间的关联。
Channel:通道(信道)
多路复用连接中的一条独立的双向数据流通道。
信道是建立在真实的TCP连接内的 虚拟链接
AMQP命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,都是通过信道完成的
因为对于操作系统来说,建立和销毁TCP连接都是非常昂贵的开销,所以引入了信道的概念,用来复用TCP连接
Connection:网络连接,比如一个TCP连接。
Publisher:消息的生产者,也是一个向交换器发布消息的客户端应用程序。
Consumer:消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。
Message:消息
消息是不具名的,它是由消息头和消息体组成。
消息体是不透明的,而消息头则是由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(优先级)、delivery-mode(消息可能需要持久性存储[消息的路由模式])等。

怎么用RabbitMQ

想要安装RabbitMQ,必须先安装erlang语言环境,类似安装tomcat,必须先安装JDK
查看匹配的版本:https://www.rabbitmq.com/which-erlang.html

RabbitMQ安装启动

erlang下载:https://dl.bintray.com/rabbitmq-erlang/rpm/erlang
socat下载:http://repo.iotti.biz/CentOS/7/x86_64/socat-1.7.3.2-5.el7.lux.x86_64.rpm
RabbitMQ下载:https://www.rabbitmq.com/install-rpm.html#downloads

安装

[root@localhost opt]# rpm -ivh erlang-21.3.8.16-1.el7.x86_64.rpm
[root@localhost opt]# rpm -ivh socat-1.7.3.2-5.el7.lux.x86_64.rpm
[root@localhost opt]# rpm -ivh rabbitmq-server-3.8.6-1.el7.noarch.rpm

启动后台管理插件

[root@localhost opt]# rabbitmq-plugins enable rabbitmq_management

启动RabbitMQ

[root@localhost opt]# systemctl start rabbitmq-server.service
[root@localhost opt]# systemctl status rabbitmq-server.service
[root@localhost opt]# systemctl restart rabbitmq-server.service
[root@localhost opt]# systemctl stop rabbitmq-server.service

查看进程

[root@localhost opt]# ps -ef | grep rabbitmq

测试

1. 关闭防火墙: systemctl stop firewalld
2. 浏览器输入:http://ip:15672
3. 默认帐号密码:guest,guest用户默认不允许远程连接

1. 创建账号

[root@localhost opt]# rabbitmqctl add_user laosun 123456

2. 设置用户角色

[root@localhost opt]# rabbitmqctl set_user_tags laosun administrator

3. 设置用户权限

[root@localhost opt]# rabbitmqctl set_permissions -p "/" laosun ".*"
".*" ".*"

4. 查看当前用户和角色

[root@localhost opt]# rabbitmqctl list_users

5. 查看当前用户和角色

[root@localhost opt]# rabbitmqctl change_password laosun 123123

 

 

 管理界面介绍

overview:概览
connections:查看链接情况
channels:信道(通道)情况
Exchanges:交换机(路由)情况,默认4类7个
Queues:消息队列情况
Admin:管理员列表

端口:

  5672:RabbitMQ提供给编程语言客户端链接的端口
  15672:RabbitMQ管理界面的端口
  25672:RabbitMQ集群的端口

RabbitMQ快速入门

依赖

<dependencies>
        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>5.7.3</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.25</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.9</version>
        </dependency>
    </dependencies>

日志依赖log4j(可选项)

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %m%n
log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=rebbitmq.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %l %m%n
log4j.rootLogger=debug, stdout,file

 创建连接

public class ConnectionUtil {
    public static Connection getConnection() throws  Exception{
        //1.创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //2.在工厂对象中设置MQ的连接信息(ip,port,vhost,username,password)
        factory.setHost("192.168.199.128");
        factory.setPort(5672);
        factory.setVirtualHost("/lagou");
        factory.setUsername("zhf");
        factory.setPassword("123456");
        //3.通过工厂获得与MQ的连接
        Connection connection = factory.newConnection();
        return connection;
    }

    public static void main(String[] args) throws  Exception{
        Connection connection = getConnection();
        System.out.println("connection = " + connection);
        connection.close();;
    }
}

RabbitMQ模式

RabbitMQ提供了6种消息模型,但是第6种其实是RPC,并不是MQ,因此我们只学习前5种

在线手册:https://www.rabbitmq.com/getstarted.html

 

 5种消息模型,大体分为两类:

1和2属于点对点

3、4、5属于发布订阅模式(一对多)

点对点模式:P2P(point to point)模式包含三个角色:

消息队列(queue),发送者(sender),接收者(receiver)

每个消息发送到一个特定的队列中,接收者从中获得消息

队列中保留这些消息,直到他们被消费或超时

特点:

1. 每个消息只有一个消费者,一旦消费,消息就不在队列中了

2. 发送者和接收者之间没有依赖性,发送者发送完成,不管接收者是否运行,都不会影响消息发送到队列中(我给你发微信,不管你看不看手机,反正我发完了)

3. 接收者成功接收消息之后需向对象应答成功(确认)

如果希望发送的每个消息都会被成功处理,那需要P2P

发布订阅模式:publish(Pub)/subscribe(Sub)

pub/sub模式包含三个角色:交换机(exchange),发布者(publisher),订阅者(subcriber)

多个发布者将消息发送交换机,系统将这些消息传递给多个订阅者

特点:

1. 每个消息可以有多个订阅者

2. 发布者和订阅者之间在时间上有依赖,对于某个交换机的订阅者,必须创建一个订阅后,才能消费发布者的消息

3. 为了消费消息,订阅者必须保持运行状态;类似于,看电视直播。

如果希望发送的消息被多个消费者处理,可采用本模式

简单模式

Introduction

RabbitMQ is a message broker: it accepts and forwards messages. You can think about it as a post office: when you put the mail that you want posting in a post box, you can be sure

 that Mr. or Ms. Mailperson will eventually deliver the mail to your recipient. In this analogy, RabbitMQ is a post box, a post office and a postman.

译文:RabbitMQ是一个消息代理:它接收和转发消息。你可以把它想象成一个邮局:当你把你想要寄的邮件放到一个邮箱里,你可以确定邮递员先生或女士最终会把邮件送到你的收件人那里。在这个类比中,RabbitMQ是一个邮箱、一个邮局和一个邮递员。

RabbitMQ本身只是接收,存储和转发消息,并不会对信息进行处理!

类似邮局,处理信件的应该是收件人而不是邮局!

 

 生产者P

package simplest;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import util.ConnectionUtil;

/**
 * @BelongsProject: lagou-rabbitmq
 * @Author: GuoAn.Sun
 * @CreateTime: 2020-08-10 14:59
 * @Description: 消息生产者
 */
public class Sender {
    public static void main(String[] args) throws Exception {
        String msg = "老孙:Hello,java!";

        // 1.获得连接
        Connection connection = ConnectionUtil.getConnection();
        // 2.在连接中创建通道(信道)
        Channel channel = connection.createChannel();
        // 3.创建消息队列(1,2,3,4,5)
        /*
        参数1:队列的名称
        参数2:队列中的数据是否持久化
        参数3:是否排外(是否支持扩展,当前队列只能自己用,不能给别人用)
        参数4:是否自动删除(当队列的连接数为0时,队列会销毁,不管队列是否还存保存数据)
        参数5:队列参数(没有参数为null)
         */
        channel.queueDeclare("queue1",false,false,false,null);
        // 4.向指定的队列发送消息(1,2,3,4)
        /*
        参数1:交换机名称,当前是简单模式,也就是P2P模式,没有交换机,所以名称为""
        参数2:目标队列的名称
        参数3:设置消息的属性(没有属性则为null)
        参数4:消息的内容(只接收字节数组)
         */
        channel.basicPublish("","queue1",null,msg.getBytes());
        System.out.println("发送:" + msg);
        // 5.释放资源
        channel.close();
        connection.close();
    }
}

启动生产者,即可前往管理端查看队列中的信息,会有一条信息没有处理和确认

 

 消费者C

package simplest;

import com.rabbitmq.client.*;
import util.ConnectionUtil;

import java.io.IOException;

/**
 * @BelongsProject: lagou-rabbitmq
 * @Author: GuoAn.Sun
 * @CreateTime: 2020-08-10 15:08
 * @Description: 消息接收者
 */
public class Recer {
    public static void main(String[] args) throws  Exception {
        // 1.获得连接
        Connection connection = ConnectionUtil.getConnection();
        // 2.获得通道(信道)
        Channel channel = connection.createChannel();
        // 3.从信道中获得消息
        DefaultConsumer consumer = new DefaultConsumer(channel){
            @Override //交付处理(收件人信息,包裹上的快递标签,协议的配置,消息)
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                // body就是从队列中获取的消息
                String s = new String(body);
                System.out.println("接收 = " + s);
            }
        };
        // 4.监听队列 true:自动消息确认
        channel.basicConsume("queue1", true,consumer);
    }
}

消息确认机制ACK

通过刚才的案例可以看出,消息一旦被消费,消息就会立刻从队列中移除

RabbitMQ如何得知消息被消费者接收?

  • 如果消费者接收消息后,还没执行操作就抛异常宕机导致消费失败,但是RabbitMQ无从得知,这样消息就丢失了
  • 因此,RabbitMQ有一个ACK机制,当消费者获取消息后,会向RabbitMQ发送回执ACK,告知消息已经被接收
  • ACK:(Acknowledge character)即是确认字符,在数据通信中,接收站发给发送站的一种传输类控制字符。表示发来的数据已确认接收无误我们在使用http请求时,http的状态码200就是告诉我们服务器执行成功
  • 整个过程就想快递员将包裹送到你手里,并且需要你的签字,并拍照回执
  • 不过这种回执ACK分为两种情况:
    • 自动ACK:消息接收后,消费者立刻自动发送ACK(快递放在快递柜)
    • 手动ACK:消息接收后,不会发送ACK,需要手动调用(快递必须本人签收)
  • 两种情况如何选择,需要看消息的重要性:
    • 如果消息不太重要,丢失也没有影响,自动ACK会比较方便
    • 如果消息非常重要,最好消费完成手动ACK,如果自动ACK消费后,RabbitMQ就会把消息从队列中删除,如果此时消费者抛异常宕机,那么消息就永久丢失了

修改手动消息确认

// false:手动消息确认
channel.basicConsume("queue1", false, consumer);

结果如下:

 

 解决问题:

package simplest;

import com.rabbitmq.client.*;
import util.ConnectionUtil;

import java.io.IOException;

/**
 * @BelongsProject: lagou-rabbitmq
 * @Author: GuoAn.Sun
 * @CreateTime: 2020-08-10 15:08
 * @Description: 消息接收者,加入ACK确认机制
 */
public class RecerByACK {
    public static void main(String[] args) throws  Exception {
        // 1.获得连接
        Connection connection = ConnectionUtil.getConnection();
        // 2.获得通道(信道)
        final Channel channel = connection.createChannel();
        // 3.从信道中获得消息
        DefaultConsumer consumer = new DefaultConsumer(channel){
            @Override //交付处理(收件人信息,包裹上的快递标签,协议的配置,消息)
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                // body就是从队列中获取的消息
                String s = new String(body);
                System.out.println("接收 = " + s);
                // 手动确认(收件人信息,是否同时确认多个消息)
                channel.basicAck(envelope.getDeliveryTag(),false);
            }
        };
        // 4.监听队列 false:手动消息确认
        channel.basicConsume("queue1", false,consumer);
    }
}

工作队列模式

 

  •  之前我们学习的简单模式,一个消费者来处理消息,如果生产者生产消息过快过多,而消费者的能力有限,就会产生消息在队列中堆积(生活中的滞销)
  • 一个烧烤师傅,一次烤50支羊肉串,就一个人吃的话,烤好的肉串会越来越多,怎么处理?
  • 多招揽客人进行消费即可。当我们运行许多消费者程序时,消息队列中的任务会被众多消费者共享,但其中某一个消息只会被一个消费者获取(100支肉串20个人吃,但是其中的某支肉串只能被一个人吃)

生产者P

package work;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import util.ConnectionUtil;

/**
 * @BelongsProject: lagou-rabbitmq
 * @Author: GuoAn.Sun
 * @CreateTime: 2020-08-10 14:59
 * @Description: 消息生产者
 */
public class Sender {
    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare("test_work_queue",false,false,false,null);
        for(int i = 1;i<=100;i++) {
            String msg = "羊肉串 --> " + i;
            channel.basicPublish("", "test_work_queue", null, msg.getBytes());
            System.out.println("新鲜出炉:" + msg);
        }
        channel.close();
        connection.close();
    }
}

消费者1

package work;

import com.rabbitmq.client.*;
import util.ConnectionUtil;

import java.io.IOException;

/**
 * @BelongsProject: lagou-rabbitmq
 * @Author: GuoAn.Sun
 * @CreateTime: 2020-08-10 15:08
 * @Description: 消费者1
 */
public class Recer1 {
    static int i = 1; // 统计吃掉羊肉串的数量
    public static void main(String[] args) throws  Exception {
        Connection connection = ConnectionUtil.getConnection();
        final Channel channel = connection.createChannel();
        // queueDeclare() 此方法有双重作用,如果队列不存在,就创建;如果队列存在,则获取
        channel.queueDeclare("test_work_queue",false,false,false,null);
        channel.basicQos(1);
        DefaultConsumer consumer = new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String s = new String(body);
                System.out.println("【顾客1】吃掉 " + s+" ! 总共吃【"+i+++"】串!");
                // 模拟网络延迟
                try{
                    Thread.sleep(200);
                }catch (Exception e){

                }
                // 手动确认(收件人信息,是否同时确认多个消息)
                channel.basicAck(envelope.getDeliveryTag(),false);
            }
        };
        // 4.监听队列 false:手动消息确认
        channel.basicConsume("test_work_queue", false,consumer);
    }
}

消费者2

package work;

import com.rabbitmq.client.*;
import util.ConnectionUtil;

import java.io.IOException;

/**
 * @BelongsProject: lagou-rabbitmq
 * @Author: GuoAn.Sun
 * @CreateTime: 2020-08-10 15:08
 * @Description: 消费者2
 */
public class Recer2 {
    static int i = 1; // 统计吃掉羊肉串的数量
    public static void main(String[] args) throws  Exception {
        Connection connection = ConnectionUtil.getConnection();
        final Channel channel = connection.createChannel();
        // queueDeclare() 此方法有双重作用,如果队列不存在,就创建;如果队列存在,则获取
        channel.queueDeclare("test_work_queue",false,false,false,null);
        channel.basicQos(1);
        DefaultConsumer consumer = new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String s = new String(body);
                System.out.println("【顾客2】吃掉 " + s+" ! 总共吃【"+i+++"】串!");
                // 模拟网络延迟
                try{
                    Thread.sleep(900);
                }catch (Exception e){

                }
                // 手动确认(收件人信息,是否同时确认多个消息)
                channel.basicAck(envelope.getDeliveryTag(),false);
            }
        };
        // 4.监听队列 false:手动消息确认
        channel.basicConsume("test_work_queue", false,consumer);
    }
}

 

先运行2个消费者,排队等候消费(取餐),再运行生产者开始生产消息(烤肉串)
虽然两个消费者的消费速度不一致(线程休眠时间),但是消费的数量却是一致的,各消费50个消息

  • 例如:工作中,A同学编码速率高,B同学编码速率低,两个人同时开发一个项目,A10天完成,B30天完成,A完成自己的编码部分,就无所事事了,等着B完成就可以了,这样是不可以的,应该遵循“能者多劳”
  • 效率高的多干点,效率低的少干点

看下面官网是如何给出解决思路的:

公平的分配

  • 您可能已经注意到分派仍然不能完全按照我们的要求工作。例如,如果有两个员工,当所有奇怪的消息都很重,甚至消息都很轻时,一个员工会一直很忙,而另一个人几乎什么工作都不做。好吧,RabbitMQ对此一无所知,它仍然会均匀地分派消息。
  • 这是因为RabbitMQ只在消息进入队列时发送消息。它不查看用户未确认消息的数量。它只是盲目地将每条第n个消息分派给第n个消费者。
  • 为了克服这个问题,我们可以使用设置为prefetchCount = 1的basicQos方法。这告诉RabbitMQ一次不要给一个worker发送一条以上的消息。或者,换句话说,在worker处理并确认前一个消息之前,不要向它发送新消息。相反,它将把它分派到下一个不繁忙的worker。
// 声明队列(此处为消费者,不是声明创建队列,而且获取,二者代码相同)出餐口排队
channel.queueDeclare("test_work_queue",false,false,false,null);
// 可以理解为:快递一个一个送,送完一个再送下一个,速度快的送件就多
channel.basicQos(1);

能者多劳必须要配合手动的ACK机制才生效

面试题:避免消息堆积?

1. workqueue,多个消费者监听同一个队列
2. 接收到消息后,通过线程池,异步消费

发布订阅模式

看官网:

发布-订阅

  • 在上一篇教程中,我们创建了一个工作队列。工作队列背后的假设是,每个任务都被准确地交付给一个工作者。在这一部分中,我们将做一些完全不同的事情——将消息传递给多个消费者。此模式称为“发布/订阅”。
  • 为了演示这个模式,我们将构建一个简单的日志记录系统。它将由两个程序组成——第一个将发送日志消息,第二个将接收和打印它们。
  • 在我们的日志系统中,接收程序的每一个正在运行的副本都将获得消息。这样我们就可以运行一个接收器并将日志指向磁盘;与此同时,我们可以运行另一个接收器并在屏幕上看到日志。基本上,发布的日志消息将广播到所有接收方。

生活中的案例:就是玩抖音快手,众多粉丝关注一个视频主,视频主发布视频,所有粉丝都可以得到视频通知

 

 

 

上图中,X就是视频主,红色的队列就是粉丝。binding是绑定的意思(关注)
P生产者发送信息给X路由,X将信息转发给绑定X的队列

 

 

 

X队列将信息通过信道发送给消费者,从而进行消费
整个过程,必须先创建路由

路由在生产者程序中创建
因为路由没有存储消息的能力,当生产者将信息发送给路由后,消费者还没有运行,所以没有队列,路由并不知道将信息发送给谁
运行程序的顺序:

  1. MessageSender
  2. MessageReceiver1和MessageReceiver2
  3. MessageSender

 

生产者

public class Sender {
  public static void main(String[] args) throws Exception {
    Connection connection = ConnectionUtil.getConnection();
    Channel channel = connection.createChannel();
    // 声明路由(路由名,路由类型)
    // fanout:不处理路由键(只需要将队列绑定到路由上,发送到路由的消息都会被转发到与该
路由绑定的所有队列上)
    channel.exchangeDeclare("test_exchange_fanout", "fanout");
    String msg = "hello,大家好!";
    channel.basicPublish("test_exchange_fanout", "", null, msg.getBytes());
    System.out.println("生产者:" + msg);
    channel.close();
    connection.close();
 }
}

消费者1

public class Recer1 {
  public static void main(String[] args) throws  Exception {
    Connection connection = ConnectionUtil.getConnection();
    Channel channel = connection.createChannel();
    // 声明队列
  
 channel.queueDeclare("test_exchange_fanout_queue_1",false,false,false,null);
    // 绑定路由(关注)
    /*
    参数1:队列名
    参数2:交换器名称
    参数3:路由key(暂时无用,""即可)
    */
    channel.queueBind("test_exchange_fanout_queue_1",
"test_exchange_fanout", "");
    DefaultConsumer consumer = new DefaultConsumer(channel){
      @Override
      public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
        String s = new String(body);
        System.out.println("【消费者1】 = " + s);
     }
   };
    // 4.监听队列 true:自动消息确认
    channel.basicConsume("test_exchange_fanout_queue_1", true,consumer);
 }
}

消费者2

将消费者1代码中的1修改为2即可,具体代码略

路由模式

 

路由会根据类型进行定向分发消息给不同的队列,如图所示
可以理解为是快递公司的分拣中心,整个小区,东面的楼小张送货,西面的楼小王送货

生产者

public class Sender {
  public static void main(String[] args) throws Exception {
    Connection connection = ConnectionUtil.getConnection();
    Channel channel = connection.createChannel();
    // 声明路由(路由名,路由类型)
    // direct:根据路由键进行定向分发消息
    channel.exchangeDeclare("test_exchange_direct", "direct");
    String msg = "用户注册,【userid=S101】";
    channel.basicPublish("test_exchange_direct", "insert", null,
msg.getBytes());
    System.out.println("[用户系统]:" + msg);
    channel.close();
    connection.close();
 }
}

消费者1

public class Recer1 {
  public static void main(String[] args) throws  Exception {
    Connection connection = ConnectionUtil.getConnection();
    Channel channel = connection.createChannel();
    // 声明队列
  
 channel.queueDeclare("test_exchange_direct_queue_1",false,false,false,null);
    // 绑定路由(如果路由键的类型是 添加,删除,修改 的话,绑定到这个队列1上)
     channel.queueBind("test_exchange_direct_queue_1",
"test_exchange_direct", "insert");
    channel.queueBind("test_exchange_direct_queue_1",
"test_exchange_direct", "update");
    channel.queueBind("test_exchange_direct_queue_1",
"test_exchange_direct", "delete");
    DefaultConsumer consumer = new DefaultConsumer(channel){
      @Override
      public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
        String s = new String(body);
        System.out.println("【消费者1】 = " + s);
     }
   };
    // 4.监听队列 true:自动消息确认
    channel.basicConsume("test_exchange_direct_queue_1", true,consumer);
 }
}

消费者2

public class Recer2 {
  public static void main(String[] args) throws  Exception {
    Connection connection = ConnectionUtil.getConnection();
    Channel channel = connection.createChannel();
    // 声明队列
  
 channel.queueDeclare("test_exchange_direct_queue_2",false,false,false,null);
    // 绑定路由(如果路由键的类型是 查询 的话,绑定到这个队列2上)
    channel.queueBind("test_exchange_direct_queue_2",
"test_exchange_direct", "select");
    DefaultConsumer consumer = new DefaultConsumer(channel){
      @Override
      public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
        String s = new String(body);
        System.out.println("【消费者2】 = " + s);
     }
   };
    // 4.监听队列 true:自动消息确认
    channel.basicConsume("test_exchange_direct_queue_2", true,consumer);
 }
}

1. 记住运行程序的顺序,先运行一次sender(创建路由器),
2. 有了路由器之后,在创建两个Recer1和Recer2,进行队列绑定
3. 再次运行sender,发出消息

通配符模式

 

 

和路由模式90%是一样的。
唯独的区别就是路由键支持模糊匹配
匹配符号

  • *:只能匹配一个词(正好一个词,多一个不行,少一个也不行)
  • #:匹配0个或更多个词

看一下官网案例:

Q1绑定了路由键 *.orange.* Q2绑定了路由键 *.*.rabbit 和 lazy.#
下面生产者的消息会被发送给哪个队列?

quick.orange.rabbit # Q1 Q2
lazy.orange.elephant # Q1 Q2
quick.orange.fox # Q1
lazy.brown.fox # Q2
lazy.pink.rabbit # Q2
quick.brown.fox # 无
orange # 无
quick.orange.male.rabbit # 无

生产者

public class Sender {
  public static void main(String[] args) throws Exception {
    Connection connection = ConnectionUtil.getConnection();
    Channel channel = connection.createChannel();
    // 声明路由(路由名,路由类型)
    // topic:模糊匹配的定向分发
    channel.exchangeDeclare("test_exchange_topic", "topic");
    String msg = "商品降价";
    channel.basicPublish("test_exchange_topic", "product.price", null,
msg.getBytes());
    System.out.println("[用户系统]:" + msg);
    channel.close();
    connection.close();
 }
}

消费者1

public class Recer1 {
  public static void main(String[] args) throws  Exception {
    Connection connection = ConnectionUtil.getConnection();
    Channel channel = connection.createChannel();
    // 声明队列
  
 channel.queueDeclare("test_exchange_topic_queue_1",false,false,false,null);
    // 绑定路由(绑定 用户相关 的消息)
    channel.queueBind("test_exchange_topic_queue_1", "test_exchange_topic",
"user.#");
    DefaultConsumer consumer = new DefaultConsumer(channel){
      @Override
      public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
        String s = new String(body);
        System.out.println("【消费者1】 = " + s);
     }
   };
    // 4.监听队列 true:自动消息确认
    channel.basicConsume("test_exchange_topic_queue_1", true,consumer);
 }
}

消费者2

public class Recer2 {
  public static void main(String[] args) throws  Exception {
    Connection connection = ConnectionUtil.getConnection();
    Channel channel = connection.createChannel();
    // 声明队列
  
 channel.queueDeclare("test_exchange_topic_queue_2",false,false,false,null);
    // 绑定路由(绑定 商品和订单相关 的消息)
    channel.queueBind("test_exchange_topic_queue_2", "test_exchange_topic",
"product.#");
    channel.queueBind("test_exchange_topic_queue_2", "test_exchange_topic",
"order.#");
    DefaultConsumer consumer = new DefaultConsumer(channel){
      @Override
      public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
        String s = new String(body);
        System.out.println("【消费者2】 = " + s);
     }
   };
    // 4.监听队列 true:自动消息确认
    channel.basicConsume("test_exchange_topic_queue_2", true,consumer);
 }
}

持久化

消息的可靠性是RabbitMQ的一大特色,那么RabbitMQ是如何避免消息丢失?

  • 消费者的ACK确认机制,可以防止消费者丢失消息
  • 万一在消费者消费之前,RabbitMQ服务器宕机了,那消息也会丢失

想要将消息持久化,那么 路由和队列都要持久化 才可以

生产者

public class Sender {
  public static void main(String[] args) throws Exception {
    Connection connection = ConnectionUtil.getConnection();
    Channel channel = connection.createChannel();
    // 声明路由(路由名,路由类型,持久化)
    channel.exchangeDeclare("test_exchange_topic", "topic",true);
    String msg = "商品降价";
    // 发送消息(第三个参数作用是让消息持久化)
    channel.basicPublish("test_exchange_topic", "product.price",
MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes());
    System.out.println("[用户系统]:" + msg);
    channel.close();
    connection.close();
 }
}

消费者

public class Recer1 {
  public static void main(String[] args) throws  Exception {
    Connection connection = ConnectionUtil.getConnection();
    Channel channel = connection.createChannel();
    // 声明队列( 第二个参数为true:支持持久化)
  
 channel.queueDeclare("test_exchange_topic_queue_1",true,false,false,null);
    channel.queueBind("test_exchange_topic_queue_1", "test_exchange_topic",
"user.#");
    DefaultConsumer consumer = new DefaultConsumer(channel){
      @Override
      public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
        String s = new String(body);
        System.out.println("【消费者1】 = " + s);
     }
   };
    channel.basicConsume("test_exchange_topic_queue_1", true,consumer);
 }
}

Spring整合RabbitMQ

五种消息模型,在企业中应用最广泛的就是最后一种:定向匹配topic
Spring AMQP 是基于 Spring 框架的AMQP消息解决方案,提供模板化的发送和接收消息的抽象层,提供基于消息驱动的 POJO的消息监听等,简化了我们对于RabbitMQ相关程序的开发。

生产端工程

依赖

<dependency>
  <groupId>org.springframework.amqp</groupId>
  <artifactId>spring-rabbit</artifactId>
  <version>2.0.1.RELEASE</version>
</dependency>
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-log4j12</artifactId>
  <version>1.7.25</version>
  <scope>compile</scope>
</dependency>
<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-lang3</artifactId>
  <version>3.9</version>
</dependency>

spring-rabbitmq-producer.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:rabbit="http://www.springframework.org/schema/rabbit"
   xsi:schemaLocation="
   http://www.springframework.org/schema/beans
   http://www.springframework.org/schema/beans/spring-beans.xsd
   http://www.springframework.org/schema/rabbit
   http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
  <!-- 1.配置连接 -->
  <rabbit:connection-factory
      id="connectionFactory"
      host="192.168.204.141"
      port="5672"
      username="laosun"
      password="123123"
      virtual-host="/lagou"
  />
  <!-- 2.配置队列 -->
  <rabbit:queue name="test_spring_queue_1"/>
  <!-- 3.配置rabbitAdmin:主要用于在Java代码中对理队和队列进行管理,用于创建、绑定、删
除队列与交换机,发送消息等 -->
  <rabbit:admin connection-factory="connectionFactory"/>
  <!-- 4.配置topic类型exchange;队列绑定到交换机 -->
  <rabbit:topic-exchange name="spring_topic_exchange">
    <rabbit:bindings>
      <rabbit:binding queue="test_spring_queue_1" pattern="msg.#"/>
    </rabbit:bindings>
  </rabbit:topic-exchange>
  <!-- 5. 配置消息对象json转换类 -->
  <bean id="jsonMessageConverter"
class="org.springframework.amqp.support.converter.Jackson2JsonMessageConverter"/
>
  <!-- 6. 配置RabbitTemplate(消息生产者) -->
  <rabbit:template id="rabbitTemplate"
          connection-factory="connectionFactory"
          exchange="spring_topic_exchange"
          message-converter="jsonMessageConverter"
  />
</beans>

发消息

public class Sender {
  public static void main(String[] args) {
    // 1.创建spring容器
    ClassPathXmlApplicationContext context = new
ClassPathXmlApplicationContext("spring/spring-rabbitmq-producer.xml");
    // 2.从容器中获取对象
    RabbitTemplate template = context.getBean(RabbitTemplate.class);
    // 3.发送消息
    Map<String, String> map = new HashMap();
    map.put("name", "大佬孙");
    map.put("email", "19998539@qq.com");
    template.convertAndSend("msg.user", map);
    context.close();
 }
}

消费端工程

依赖与生产者一致

spring-rabbitmq-consumer.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:rabbit="http://www.springframework.org/schema/rabbit"
   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/rabbit
   http://www.springframework.org/schema/rabbit/spring-rabbit.xsd
   http://www.springframework.org/schema/context
   http://www.springframework.org/schema/context/spring-context.xsd">
  <!-- 1. 配置连接 -->
  <rabbit:connection-factory
      id="connectionFactory"
      host="192.168.204.141"
      port="5672"
      username="laosun"
      password="123123"
      virtual-host="/lagou"
  />
  <!-- 2. 配置队列 -->
  <rabbit:queue name="test_spring_queue_1"/>
  <!-- 3.配置rabbitAdmin -->
  <rabbit:admin connection-factory="connectionFactory"/>
  <!-- 4.springIOC注解扫描包-->
  <context:component-scan base-package="listener"/>
  <!-- 5.配置监听 -->
  <rabbit:listener-container connection-factory="connectionFactory">
    <rabbit:listener ref="consumerListener" queue-
names="test_spring_queue_1" />
  </rabbit:listener-container>
</beans>

消费者

MessageListener接口用于spring容器接收到消息后处理消息
如果需要使用自己定义的类型 来实现 处理消息时,必须实现该接口,并重写onMessage()方法
当spring容器接收消息后,会自动交由onMessage进行处理

@Component
public class ConsumerListener implements MessageListener {
  // jackson提供序列化和反序列中使用最多的类,用来转换json的
  private static final ObjectMapper MAPPER = new ObjectMapper();
  public void onMessage(Message message) {
    try {
      // 将message对象转换成json
      JsonNode jsonNode = MAPPER.readTree(message.getBody());
      String name = jsonNode.get("name").asText();
      String email = jsonNode.get("email").asText();
      System.out.println("从队列中获取:【"+name+"的邮箱是:"+email+"】");
   } catch (Exception e){
      e.printStackTrace();
   }
 }
}

启动项目

public class TestRunner {
  public static void main(String[] args) throws  Exception {
    // 获得容器
    ClassPathXmlApplicationContext context = new
ClassPathXmlApplicationContext("spring/spring-rabbitmq-consumer.xml");
    // 让程序一直运行,别终止
    System.in.read();
 }
}

消息成功确认机制

在实际场景下,有的生产者发送的消息是必须保证成功发送到消息队列中,那么如何保证成功投递呢?

  • 事务机制
  • 发布确认机制

事务机制

AMQP协议提供的一种保证消息成功投递的方式,通过信道开启 transactional 模式
并利用信道 的三个方法来实现以事务方式 发送消息,若发送失败,通过异常处理回滚事务,确保消息成功投递

  • channel.txSelect(): 开启事务
  • channel.txCommit() :提交事务
  • channel.txRollback() :回滚事务

Spring已经对上面三个方法进行了封装,所以我们只能使用原始的代码演示

生产者

public class Sender {
  public static void main(String[] args) throws Exception {
    Connection connection = ConnectionUtil.getConnection();
    Channel channel = connection.createChannel();
    channel.exchangeDeclare("test_transaction", "topic");
    channel.txSelect(); // 开启事务
    try {
      channel.basicPublish("test_transaction", "product.price", null, "商品
降价1".getBytes());
      // System.out.println(1/0); // 模拟异常!
      channel.basicPublish("test_transaction", "product.price", null, "商品
降价2".getBytes());
      System.out.println("消息全部发出!");
      channel.txCommit(); // 事务提交
   }catch (Exception e){
      System.out.println("由于系统异常,消息全部撤回!");
      channel.txRollback(); // 事务回滚
      e.printStackTrace();
   }finally {
      channel.close();
      connection.close();
   }
 }
}

消费者

public class Recer {
  public static void main(String[] args) throws  Exception {
    Connection connection = ConnectionUtil.getConnection();
    Channel channel = connection.createChannel();
    channel.queueDeclare("test_transaction_queue",false,false,false,null);
    channel.queueBind("test_transaction_queue", "test_transaction",
"product.#");
    DefaultConsumer consumer = new DefaultConsumer(channel){
      @Override
      public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
        String s = new String(body);
        System.out.println("【消费者】 = " + s);
     }
   };
    channel.basicConsume("test_transaction_queue", true,consumer);
 }
}

Confirm发布确认机制

RabbitMQ为了保证消息的成功投递,采用通过AMQP协议层面为我们提供事务机制的方案,但是采用事务会大大降低消息的吞吐量
老孙我本机SSD硬盘测试结果10w条消息未开启事务,大约8s发送完毕;而开启了事务后,需要将近310s,差了30多倍。
接着老孙翻阅官网,发现官网中已标注

那么有没有更加高效的解决方式呢?答案就是采用Confirm模式。
事务效率为什么会这么低呢?试想一下:10条消息,前9条成功,如果第10条失败,那么9条消息要全部撤销回滚。太太太浪费
而confirm模式则采用补发第10条的措施来完成10条消息的送达

在spring中应用

spring-rabbitmq-producer.xml

<!--1.配置连接,启动生产者确认机制: publisher-confirms="true"-->
<rabbit:connection-factory id="connectionFactory"
             host="192.168.204.141"
             port="5672"
             username="laosun"
             password="123123"
             virtual-host="/lagou"
             publisher-confirms="true"
/>
<!--6.配置rabbitmq的模版,添加确认回调处理类:confirm-
callback="msgSendConfirmCallback"-->
<rabbit:template id="rabbitTemplate"
        connection-factory="connectionFactory"
        exchange="spring_topic_exchange"
        message-converter="jsonMessageConverter"
        confirm-callback="msgSendConfirmCallback"/>
<!--7.确认机制处理类-->
<bean id="msgSendConfirmCallback" class="confirm.MsgSendConfirmCallback"/>

消息确认处理类

package confirm;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.support.CorrelationData;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* @BelongsProject: spring-rabbitmq-producer
* @Author: GuoAn.Sun
* @Description: 确认机制
*/
@Component
public class MsgSendConfirmCallback implements
RabbitTemplate.ConfirmCallback {
  public void confirm(CorrelationData correlationData, boolean b, String
s) {
    if (b){
      System.out.println("消息确认成功!!");
   } else {
      System.out.println("消息确认失败。。。");
   // 如果本条消息一定要发送到队列中,例如下订单消息,我们可以采用消息补发
      // 采用递归(固定次数,不可无限)或 redis+定时任务
   }
 }
}

log4j.properties

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %m%n
log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=rabbitmq.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %l
%m%n
log4j.rootLogger=debug, stdout,file

发送消息

public class Sender {
  public static void main(String[] args) {
    ClassPathXmlApplicationContext context = new
ClassPathXmlApplicationContext("spring/spring-rabbitmq-producer.xml");
    RabbitTemplate rabbitTemplate =
context.getBean(RabbitTemplate.class);
    Map<String,String> map = new HashMap<String, String>();
    map.put("name", "吕布");
    map.put("email", "666@qq.com");
    // 第一个参数是路由名称,
    // 不写,则使用spring容器中创建的路由
    // 乱写一个,因为路由名错误导致报错,则进入消息确认失败流程
    rabbitTemplate.convertAndSend("x","msg.user",map);
    System.out.println("ok");
    context.close();
 }
}

消费端限流

在沙漠中行走,3天不喝水,突然喝水,如果使劲喝,容易猝死,要一口一口慢慢喝

我们 Rabbitmq 服务器积压了成千上万条未处理的消息,然后随便打开一个消费者客户端,就会出现这样的情况: 巨量的消息瞬间全部喷涌推送过来,但是单个客户端无法同时处理这么多数据,就会被压垮崩溃

所以,当数据量特别大的时候,我们对生产端限流肯定是不科学的,因为有时候并发量就是特别大,有时候并发量又特别少,这是用户的行为,我们是无法约束的

所以我们应该对消费端限流,用于保持消费端的稳定

例如:汽车企业不停的生产汽车,4S店有好多库存车卖不出去,但是也不会降价处理,就是要保证市值的稳定,如果生产多少台,就卖多少台,不管价格的话,市场就乱了,所以我们要用不变的价格来稳住消费者购车,才能平稳发展

RabbitMQ 提供了一种 Qos (Quality of Service,服务质量)服务质量保证功能

  • 即在非自动确认消息的前提下,如果一定数目的消息未被确认前,不再进行消费新的消息

生产者使用循环发出多条消息

for(int i = 1;i<=10;i++) {
  rabbitTemplate.convertAndSend("msg.user", map);
  System.out.println("消息已发出...");
}

生产10条堆积未处理的消息

 

 

 消费者进行限流处理

<!--5.配置监听-->
<!-- prefetch="3" 一次性消费的消息数量。会告诉 RabbitMQ 不要同时给一个消费者推送多于
N 个消息,一旦有 N 个消息还没有ack,则该 consumer 将阻塞,直到消息被ack-->
<!-- acknowledge-mode: manual 手动确认-->
<rabbit:listener-container connection-factory="connectionFactory"
prefetch="3" acknowledge="manual">
  <rabbit:listener ref="consumerListener" queue-
names="test_spring_queue_1" />
</rabbit:listener-container>
// AbstractAdaptableMessageListener用于在spring容器接收到消息后用于处理消息的抽象
基类
@Component
public class ConsumerListener extends AbstractAdaptableMessageListener {
  // jackson提供序列化和反序列中使用最多的类,用来转换json的
  private static final ObjectMapper MAPPER = new ObjectMapper();
  public void onMessage(Message message, Channel channel) throws Exception
{
    try {
      // String str = new String(message.getBody());
      // 将message对象转换成json
      JsonNode jsonNode = MAPPER.readTree(message.getBody());
      String name = jsonNode.get("name").asText();
      String email = jsonNode.get("email").asText();
      System.out.println("从队列中获取:【"+name+"的邮箱是:"+email+"】");
      long deliveryTag =
message.getMessageProperties().getDeliveryTag();
      //确认收到(参数1,参数2)
      /*
      参数1:RabbitMQ 向该 Channel 投递的这条消息的唯一标识 ID,是一个单调递
增的正整数,delivery_tag 的范围仅限于 Channel
      参数2:为了减少网络流量,手动确认可以被批处理,当该参数为 true 时,则可以
一次性确认 delivery_tag 小于等于传入值的所有消息
      */   
      channel.basicAck(deliveryTag , true);
      Thread.sleep(3000);
      System.out.println("休息三秒然后再接收消息");
   } catch (Exception e){
      e.printStackTrace();
   }
 }
}

每次确认接收3条消息

 

 

 过期时间TTL

Time To Live:生存时间、还能活多久,单位毫秒

在这个周期内,消息可以被消费者正常消费,超过这个时间,则自动删除(其实是被称为dead message并投入到死信队列,无法消费该消息)

RabbitMQ可以对消息队列设置TTL

设置队列TTL

spring-rabbitmq-producer.xml

<!--2.重新配置一个队列,同时,对队列中的消息设置过期时间-->
<rabbit:queue name="test_spring_queue_ttl" auto-declare="true">
  <rabbit:queue-arguments>
    <entry key="x-message-ttl" value-type="long" value="5000"></entry>
  </rabbit:queue-arguments>
</rabbit:queue>

 

 

 5秒之后,消息自动删除

 

 

 设置消息TTL

设置某条消息的ttl,只需要在创建发送消息时指定即可

<!--2.配置队列-->
<rabbit:queue name="test_spring_queue_ttl_2">
package test;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.util.HashMap;
import java.util.Map;
/**
* @BelongsProject: spring-rabbitmq-producer
* @Author: GuoAn.Sun
* @Description: 生产者
*/
public class Sender2 {
  public static void main(String[] args) {
     ClassPathXmlApplicationContext context = new
ClassPathXmlApplicationContext("spring/spring-rabbitmq-producer.xml");
    RabbitTemplate rabbitTemplate = context.getBean(RabbitTemplate.class);
    // 创建消息配置对象
    MessageProperties messageProperties = new MessageProperties();
    // 设置消息过期时间
    messageProperties.setExpiration("6000");
    // 创建消息
    Message message = new Message("6秒后自动删除".getBytes(),
messageProperties);
    // 发送消息
    rabbitTemplate.convertAndSend("msg.user", message);
    System.out.println("消息已发出...");
    context.close();
 }
}

如果同时设置了queue和message的TTL值,则二者中较小的才会起作用

死信队列

LX(Dead Letter Exchanges)死信交换机/死信邮箱,当消息在队列中由于某些原因没有被及时消费而变成死信(dead message)后,这些消息就会被分发到DLX交换机中,而绑定DLX交换机的队列,称之为:“死信队列”

消息没有被及时消费的原因:

  • 消息被拒绝(basic.reject/ basic.nack)并且不再重新投递 requeue=false
  • 消息超时未消费
  • 达到最大队列长度

 

 

 spring-rabbitmq-producer-dlx.xml

<!--1.配置连接-->
<rabbit:connection-factory id="connectionFactory"
             host="192.168.204.141"
             port="5672"
             username="laosun"
             password="123123"
             virtual-host="/lagou"/>
<!--3.配置rabbitAdmin:主要用于在java代码中对队列的管理,用来创建,绑定,删除队列与交
换机,发送消息等-->
<rabbit:admin connection-factory="connectionFactory"/>
<!--6.配置rabbitmq的模版-->
<rabbit:template id="rabbitTemplate"
        connection-factory="connectionFactory"
        exchange="my_exchange"/>
<!--
############################################################################
##########################################-->
<!--死信队列-->
<rabbit:queue name="dlx_queue"/>
<!--定向死信交换机-->
<rabbit:direct-exchange name="dlx_exchange" >
  <rabbit:bindings>
    <rabbit:binding key="dlx_ttl" queue="dlx_queue"></rabbit:binding>
    <rabbit:binding key="dlx_max" queue="dlx_queue"></rabbit:binding>
  </rabbit:bindings>
</rabbit:direct-exchange>
<!--测试超时的队列-->
<rabbit:queue name="test_ttl_queue">
  <rabbit:queue-arguments>
    <!--队列ttl为6秒-->
    <entry key="x-message-ttl" value-type="long" value="6000"/>
    <!--超时 消息 投递给 死信交换机-->
    <entry key="x-dead-letter-exchange" value="dlx_exchange"/>
  </rabbit:queue-arguments>
</rabbit:queue>
<!--测试超长度的队列-->
<rabbit:queue name="test_max_queue">
  <rabbit:queue-arguments>
    <!--队列ttl为6秒-->
    <entry key="x-max-length" value-type="long" value="2"/>
    <!--超时 消息 投递给 死信交换机-->
    <entry key="x-dead-letter-exchange" value="dlx_exchange"/>
  </rabbit:queue-arguments>
</rabbit:queue>
<!--定向测试交换机-->
<rabbit:direct-exchange name="my_exchange" >
  <rabbit:bindings>
    <rabbit:binding key="dlx_ttl" queue="test_ttl_queue">
</rabbit:binding>
    <rabbit:binding key="dlx_max" queue="test_max_queue">
</rabbit:binding>
  </rabbit:bindings>
</rabbit:direct-exchange>

发消息进行测试

public class SenderDLX {
  public static void main(String[] args) {
    ClassPathXmlApplicationContext context = new
ClassPathXmlApplicationContext("spring/spring-rabbitmq-producer-dlx.xml");
    RabbitTemplate rabbitTemplate =
context.getBean(RabbitTemplate.class);
// rabbitTemplate.convertAndSend("dlx_ttl", "测试超时".getBytes());
rabbitTemplate.convertAndSend("dlx_max", "测试长度1".getBytes());
    rabbitTemplate.convertAndSend("dlx_max", "测试长度2".getBytes());
    rabbitTemplate.convertAndSend("dlx_max", "测试长度3".getBytes());
    System.out.println("消息已发出...");
    context.close();
 }
}

延迟队列

延迟队列:TTL + 死信队列的合体

死信队列只是一种特殊的队列,里面的消息仍然可以消费

在电商开发部分中,都会涉及到延时关闭订单,此时延迟队列正好可以解决这个问题

生产者

沿用上面死信队列案例的超时测试,超时时间改为订单关闭时间即可

消费者

spring-rabbitmq-consumer.xml

<!-- 监听死信队列 -->
<rabbit:listener-container connection-factory="connectionFactory" prefetch="3"
acknowledge="manual">
  <rabbit:listener ref="consumerListener" queue-names="dlx_queue" />
</rabbit:listener-container>

RabbitMQ集群

rabbitmq有3种模式,但集群模式是2种。详细如下:

单一模式:即单机情况不做集群,就单独运行一个rabbitmq而已。之前我们一直在用

普通模式:默认模式,以两个节点(A、B)为例来进行说明

  • 当消息进入A节点的Queue后,consumer从B节点消费时,RabbitMQ会在A和B之间创建临时通道进行消息传输,把A中的消息实体取出并经过通过交给B发送给consumer
  • 当A故障后,B就无法取到A节点中未消费的消息实体
    • 如果做了消息持久化,那么得等A节点恢复,然后才可被消费
    • 如果没有持久化的话,就会产生消息丢失的现象
  • 镜像模式:非常经典的 mirror 镜像模式,保证 100% 数据不丢失。
    • 高可靠性解决方案,主要就是实现数据的同步,一般来讲是 2 - 3 个节点实现数据同步
    • 对于 100% 数据可靠性解决方案,一般是采用 3 个节点。
    • 在实际工作中也是用得最多的,并且实现非常的简单,一般互联网大厂都会构建这种镜像集群模式
  • 还有主备模式,远程模式,多活模式等,本次课程不作为重点,可自行查阅资料了解

集群搭建

前置条件:准备两台linux,并安装好rabbitmq

集群步骤如下:

1. 修改 /etc/hosts 映射文件

1号服务器:

127.0.0.1 A localhost localhost.localdomain localhost4
localhost4.localdomain4
::1    A localhost localhost.localdomain localhost6
localhost6.localdomain6
192.168.204.141 A
192.168.204.142 B

2号服务器:

127.0.0.1 B localhost localhost.localdomain localhost4
localhost4.localdomain4
::1    B localhost localhost.localdomain localhost6
localhost6.localdomain6
192.168.204.141 A
192.168.204.142 B

2. 相互通信,cookie必须保持一致,同步 rabbitmq的cookie 文件:跨服务器拷贝 .erlang.cookie

(隐藏文件,使用 ls -all 显示)

[root@A opt]# scp /var/lib/rabbitmq/.erlang.cookie
192.168.204.142:/var/lib/rabbitmq

修改cookie文件,要重启服务器,reboot

3. 停止防火墙,启动rabbitmq服务

 

[root@A ~]# systemctl stop firewalld
[root@A ~]# systemctl start rabbitmq-server

 

4. 加入集群节点

[root@B ~]# rabbitmqctl stop_app
[root@B ~]# rabbitmqctl join_cluster rabbit@A
[root@B ~]# rabbitmqctl start_app

5. 查看节点状态

[root@B ~]# rabbitmqctl cluster_status

6. 查看管理端

搭建集群结构之后,之前创建的交换机、队列、用户都属于单一结构,在新的集群环境中是不能用的

所以在新的集群中重新手动添加用户即可(任意节点添加,所有节点共享)

[root@A ~]# rabbitmqctl add_user laosun 123123
[root@A ~]# rabbitmqctl set_user_tags laosun administrator
[root@A ~]# rabbitmqctl set_permissions -p "/" laosun ".*" ".*" ".*"

注意:当节点脱离集群还原成单一结构后,交换机,队列和用户等数据 都会重新回来

 

 此时,集群搭建完毕,但是默认采用的模式“普通模式”,可靠性不高

镜像模式

将所有队列设置为镜像队列,即队列会被复制到各个节点,各个节点状态一致

语法:set_policy {name} {pattern} {definition}

name:策略名,可自定义

pattern:队列的匹配模式(正则表达式)

"^" 可以使用正则表达式,比如"^queue_" 表示对队列名称以“queue_”开头的所有队列进行镜像,而"^"表示匹配所有的队列

definition:镜像定义,包括三个部分ha-mode, ha-params, ha-sync-mode

  • ha-mode:(High Available,高可用)模式,指明镜像队列的模式,有效值为all/exactly/nodes,当前策略模式为 all,即复制到所有节点,包含新增节点
    • all:表示在集群中所有的节点上进行镜像
      exactly:表示在指定个数的节点上进行镜像,节点的个数由ha-params指定
      nodes:表示在指定的节点上进行镜像,节点名称通过ha-params指定
  • a-params:ha-mode模式需要用到的参数
  • ha-sync-mode:进行队列中消息的同步方式,有效值为automatic和manual
[root@A ~]# rabbitmqctl set_policy xall "^" '{"ha-mode":"all"}'

通过管理端设置镜像策略

 

 HAProxy实现镜像队列的负载均衡

虽然我们在程序中访问A服务器,可以实现消息的同步,虽然在同步,但都是A服务器在接收消息,A太累
是否可以想Nginx一样,做负载均衡,A和B轮流接收消息,再镜像同步

HAProxy简介

HA(High Available,高可用),Proxy(代理)
HAProxy是一款提供高可用性,负载均衡,并且基于TCP和HTTP应用的代理软件
HAProxy完全免费
HAProxy可以支持数以万计的并发连接
HAProxy可以简单又安全的整合进架构中,同时还保护web服务器不被暴露到网络上

 

HAProxy与Nginx

OSI:(Open System Interconnection:开放式系统互联 是把网络通信的工作分为7层,分别是物理层,数据链路层,网络层,传输层,会话层,表示层和应用层)

Nginx的优点:
  • 工作在OSI第7层,可以针对http应用做一些分流的策略
  • Nginx对网络的依赖非常小,理论上能ping通就就能进行负载功能,屹立至今的绝对优势
  • Nginx安装和配置比较简单,测试起来比较方便;
  • Nginx不仅仅是一款优秀的负载均衡器/反向代理软件,它同时也是功能强大的Web应用服务器
HAProxy的优点:
  • 工作在网络4层和7层,支持TCP与Http协议,
  • 它仅仅就只是一款负载均衡软件;单纯从效率上来讲HAProxy更会比Nginx有更出色的负载均衡速度,在并发处理上也是优于Nginx的
  • 支持8种负载均衡策略 ,支持心跳检测
性能上HA胜,功能性和便利性上Nginx胜
对于Http协议,Haproxy处理效率比Nginx高。所以,没有特殊要求的时候或者一般场景,建议使用Haproxy来做Http协议负载
但如果是Web应用,那么建议使用Nginx!
总之,大家可以结合各自使用场景的特点来进行合理地选择

安装和配置

HAProxy下载:http://www.haproxy.org/download/1.8/src/haproxy-1.8.12.tar.gz
解压
[root@localhost opt]# tar -zxvf haproxy-1.8.12.tar.gz
make时需要使用 TARGET 指定内核及版本
[root@localhost opt]# uname -r 
3.10.0-514.6.2.el7.x86_64
根据内核版本选择编译参数:
 
进入目录,编译和安装
[root@localhost opt]# cd haproxy-1.8.12
[root@localhost haproxy-1.8.12]# make TARGET=linux2628 PREFIX=/usr/local/haproxy
[root@localhost haproxy-1.8.12]# make install PREFIX=/usr/local/haproxy
安装成功后,查看版本
[root@localhost haproxy-1.8.12]# /usr/local/haproxy/sbin/haproxy -v
配置启动文件,复制haproxy文件到/usr/sbin下 ,复制haproxy脚本,到/etc/init.d下
[root@localhost haproxy-1.8.12]# cp /usr/local/haproxy/sbin/haproxy /usr/sbin/
[root@localhost haproxy-1.8.12]# cp ./examples/haproxy.init /etc/init.d/haproxy
[root@localhost haproxy-1.8.12]# chmod 755 /etc/init.d/haproxy
创建系统账号
[root@localhost haproxy-1.8.12]# useradd -r haproxy
haproxy.cfg 配置文件需要自行创建
[root@localhost haproxy-1.8.12]# mkdir /etc/haproxy [root@localhost haproxy-1.8.12]# vim /etc/haproxy/haproxy.cfg
添加配置信息到haproxy.cfg
#全局配置
global
        #设置日志
        log 127.0.0.1 local0 info
        #当前工作目录
        chroot /usr/local/haproxy
        #用户与用户组
        user haproxy
        group haproxy
        #运行进程ID
        uid 99
        gid 99
        #守护进程启动
        daemon
        #最大连接数
        maxconn 4096
#默认配置
defaults
        #应用全局的日志配置
        log global
        #默认的模式mode {tcp|http|health},TCP是4层,HTTP是7层,health只返回OK
        mode tcp
        #日志类别tcplog
        option tcplog
        #不记录健康检查日志信息
        option dontlognull
        #3次失败则认为服务不可用
        retries 3
        #每个进程可用的最大连接数
        maxconn 2000
        #连接超时
        timeout connect 5s
        #客户端超时30秒,ha就会发起重新连接
        timeout client 30s
        #服务端超时15秒,ha就会发起重新连接
        timeout server 15s
#绑定配置
listen rabbitmq_cluster
        bind 192.168.199.130:5672
        #配置TCP模式
        mode tcp
        #简单的轮询
        balance roundrobin
        #RabbitMQ集群节点配置,每隔5秒对mq集群做检查,2次正确证明服务可用,3次失败证明服务不可用
        server A 192.168.199.128:5672 check inter 5000 rise 2 fall 3
        server B 192.168.199.129:5672 check inter 5000 rise 2 fall 3
#haproxy监控页面地址
listen monitor
        bind 192.168.199.130:8100
        mode http
        option httplog
        stats enable
        # 监控页面地址 http://192.168.204.143:8100/monitor
        stats uri /monitor
        stats refresh 5s
启动HAProxy
[root@localhost haproxy]# service haproxy start
访问监控中心:http://192.168.204.143:8100/monitor
 记得关闭防火墙: systemctl stop firewalld
 项目发消息,只需要将服务器地址修改为143即可,其余不变
 所有的请求都会交给HAProxy,其负载均衡给每个rabbitmq服务器

 KeepAlived搭建高可用的HAProxy集群

现在的最后一个问题暴露出来了,如果HAProxy服务器宕机,rabbitmq服务器就不可用了。所以我们需要对HAProxy也要做高可用的集群

概述

  •  Keepalived是Linux下一个轻量级别的高可用热备解决方案
  • Keepalived的作用是检测服务器的状态,它根据TCP/IP参考模型的第三、第四层、第五层交换机制检测每个服务节点的状态,如果有一台web服务器宕机,或工作出现故障,Keepalived将检测到,并将有故障的服务器从系统中剔除,同时使用其他服务器代替该服务器的工作,当服务器工作正常后Keepalived自动将服务器加入到服务器群中,这些工作全部自动完成,不需要人工干涉,需要人工做的只是修复故障的服务器。
  • keepalived基于vrrp(Virtual Router Redundancy Protocol,虚拟路由冗余协议)协议,vrrp它是一种主备(主机和备用机)模式的协议,通过VRRP可以在网络发生故障时透明的进行设备切换而不影响主机之间的数据通信
  • 两台主机之间生成一个虚拟的ip,我们称漂移ip,漂移ip由主服务器承担,一但主服务器宕机,备份服务器就会抢夺漂移ip,继续工作,有效的解决了群集中的单点故障
  • 说白了,将多台路由器设备虚拟成一个设备,对外提供统一ip(VIP)

 

 

 安装KeepAlived

修改hosts文件的地址映射

 

 安装 keepalived

[root@C ~]# yum install -y keepalived

修改配置文件(内容大改,不如删掉,重新创建)

[root@C ~]# rm -rf /etc/keepalived/keepalived.conf
[root@C ~]# vim /etc/keepalived/keepalived.conf
! Configuration File for keepalived
global_defs {
 router_id C ## 非常重要,标识本机的hostname
}
vrrp_script chk_haproxy{
 script "/etc/keepalived/haproxy_check.sh" ## 执行的脚本位置
 interval 2 ## 检测时间间隔
 weight -20 ## 如果条件成立则权重减20
}
vrrp_instance VI_1 {
 state MASTER ## 非常重要,标识主机,备用机143改为 BACKUP
 interface ens33 ## 非常重要,网卡名(ifconfig查看)
 virtual_router_id 66 ## 非常重要,自定义,虚拟路由ID号(主备节点要相同)
 priority 100 ## 优先级(0-254),一般主机的大于备机
 advert_int 1 ## 主备信息发送间隔,两个节点必须一致,默认1秒
 authentication { ## 认证匹配,设置认证类型和密码,MASTER和BACKUP必须使
用相同的密码才能正常通信
   auth_type PASS
   auth_pass 1111
 }
 track_script {
   chk_haproxy ## 检查haproxy健康状况的脚本
 }
 virtual_ipaddress { ## 简称“VIP”
    192.168.204.66/24 ## 非常重要,虚拟ip,可以指定多个,以后连接mq就用这个虚
拟ip
 }
}
virtual_server 192.168.204.66 5672 { ## 虚拟ip的详细配置
 delay_loop 6 # 健康检查间隔,单位为秒
 lb_algo rr # lvs调度算法rr|wrr|lc|wlc|lblc|sh|dh
 lb_kind NAT # 负载均衡转发规则。一般包括DR,NAT,TUN 3种
 protocol TCP # 转发协议,有TCP和UDP两种,一般用TCP
 real_server 192.168.204.143 5672 { ## 本机的真实ip
weight 1 # 默认为1,0为失效
 }
}

创建执行脚本 /etc/keepalived/haproxy_check.sh

#!/bin/bash
COUNT=`ps -C haproxy --no-header |wc -l`
if [ $COUNT -eq 0 ];then
 /usr/local/haproxy/sbin/haproxy -f /etc/haproxy/haproxy.cfg
  sleep 2
  if [ `ps -C haproxy --no-header |wc -l` -eq 0 ];then
    killall keepalived
  fi
fi

Keepalived 组之间的心跳检查并不能察觉到 HAproxy 负载是否正常,所以需要使用此脚本。

在 Keepalived 主机上,开启此脚本检测 HAproxy 是否正常工作,如正常工作,记录日志。

如进程不存在,则尝试重启 HAproxy ,2秒后检测,如果还没有,则关掉主 Keepalived ,此时备Keepalived 检测到主 Keepalived 挂掉,接管VIP,继续服务

授权,否则不能执行

[root@C etc]# chmod +x /etc/keepalived/haproxy_check.sh

启动keepalived(两台都启动)

[root@C etc]# systemctl stop firewalld
[root@C etc]# service keepalived start | stop | status | restart

查看ip情况 ip addr 或  ip a

[root@C etc]# ip a

 

 此时,安装完毕,按照上面的步骤就可以安装第二台了(服务器hostname和ip注意要修改)

 常见的网络错误:子网掩码、网关等信息要一致

 

 测试vip+端口是否提供服务(在141,A服务器上测试)

[root@A ~]# curl 192.168.204.66:5672
AMQP ## 正常提供AMQP服务,表示通过vip访问mq服务正常

 

posted @ 2021-03-10 18:00  曾鸿发  阅读(360)  评论(0编辑  收藏  举报