数据访问量大,使用缓存技术来缓解数据库的压力
不同的业务访问不同的数据库
主从读写分离
之前的缓存确实能够缓解数据库的压力,但是写和读都集中在一个数据库上,压力又来了
一个数据库负责写,一个数据库负责读
让 master(主数据库)来响应事务性(增删改)操作,让 slave(从数据库)来响应非事务性(查询)操作,然后再采用主从复制来把 master 上的事务性操作同步到 slave 数据库中
mysql 的 master/slave 就是网站的标配!
mysql 的主从复制,读写分离的基础上,mysql 的主库开始出现瓶颈
由于 MyISAM 使用表锁,所以并发性能特别差
分库分表开始流行,mysql 也提出了表分区,虽然不稳定,但我们看到了希望
开始 mysql 集群
Redis 是一种运行速度很快,并发性能很强,并且运行在内存上的 NoSql(not only sql)数据库
NoSQL 数据库和传统数据库相比的优势
CAP 理论提出就是针对分布式数据库环境的,所以,P 这个属性必须容忍它的存在,而且是必须具备的
因为 P 是必须的,那么我们需要选择的就是 A 和 C
在分布式环境下,为了保证系统可用性,通常都采取了复制的方式,避免一个节点损坏,导致系统不可用。那么就出现了每个节点上的数据出现了很多个副本的情况,而数据从一个节点复制到另外的节点时需要时间和要求网络畅通的,所以,当 P 发生时,也就是无法向某个节点复制数据时,这时候你有两个选择:
最常见的例子是读写分离,某个节点负责写入数据,然后将数据同步到其它节点,其它节点提供读取的服务,当两个节点出现通信问题时,你就面临着选择A(继续提供服务,但是数据不保证准确),C(用户处于等待状态,一直等到数据同步完成)
虽然可以在安装在 windows 操作系统,但是官方不推荐,所以我们一如既往的安装在 linux 上
上传 tar.gz 包,并解压
cd /opt
tar -zxvf redis-5.0.4.tar.gz
安装 gcc(必须有网络)
yum -y install gcc
进入 redis 目录,进行编译 + 安装
cd redis-5.0.4/
make
make install
启动进入文件夹 cd /usr/local/bin
直接启动:redis-server
,窗口会被占用,不能输入其他命令
后台运行:修改配置文件 daemonize=yes
,当你后台服务启动的时候,会写成一个进程文件运行
vi /opt/redis-5.0.4/redis.conf
#找到并修改daemonize
daemonize yes
#启动
redis-server /opt/redis-5.0.4/redis.conf
redis-cli shutdown
redis-cli -p 6379 shutdown
netstat -lntp | grep 6379
ps -ef|grep redis
redis-cli
ping #pong
# 保存数据
set k1 china
# 获取数据
get kl
redis-benchmark
命令后,命令不会自动停止,需要我们手动 ctrl+c 停止测试[root@localhost bin]# redis-benchmark
====== PING_INLINE ======
100000 requests completed in 1.80 seconds # 1.8秒处理了10万个请求,性能要看笔记本的配置高低
50 parallel clients
3 bytes payload
keep alive: 1
87.69% <= 1 milliseconds
99.15% <= 2 milliseconds
99.65% <= 3 milliseconds
99.86% <= 4 milliseconds
99.92% <= 5 milliseconds
99.94% <= 6 milliseconds
99.97% <= 7 milliseconds
100.00% <= 7 milliseconds
55524.71 requests per second # 每秒处理的请求数量
vim /opt/redis-5.0.4/redis.conf
127.0.0.1:6379> get k1 # 查询k1
"china"
127.0.0.1:6379> select 16 # 切换16号数据库
(error) ERR DB index is out of range # 数据库的下标超出了范围
127.0.0.1:6379> select 15 # 切换15号数据库
OK
127.0.0.1:6379[15]> get k1 # 查询k1
(nil) # 空
127.0.0.1:6379[15]> select 0 # 切换0号数据库
OK
127.0.0.1:6379> get k1 # 查询k1
"china"
dbsize
flushdb
flushall
模糊查询 keys 命令,有三个通配符:
keys *
keys k*
keys *e
keys *k*
keys k?
keys k??
keys r[ae]dis
exists key
:判断某个key是否存在127.0.0.1:6379> exists k1
(integer) 1 # 存在
127.0.0.1:6379> exists y1
(integer) 0 # 不存在
move key db
:移动(剪切,粘贴)键到几号库127.0.0.1:6379> move x1 8 # 将x1移动到8号库
(integer) 1 # 移动成功
127.0.0.1:6379> exists x1 # 查看当前库中是否存在x1
(integer) 0 # 不存在(因为已经移走了)
127.0.0.1:6379> select 8 # 切换8号库
OK
127.0.0.1:6379[8]> keys * # 查看当前库中的所有键
1) "x1"
ttl key
:查看键还有多久过期(-1永不过期,-2已过期)—— time to live 还能活多久127.0.0.1:6379[8]> ttl x1
(integer) -1 # 永不过期
expire key 秒
:为键设置过期时间(生命倒计时)127.0.0.1:6379[8]> set k1 v1 # 保存k1
OK
127.0.0.1:6379[8]> ttl k1 # 查看k1的过期时间
(integer) -1 # 永不过期
127.0.0.1:6379[8]> expire k1 10 # 设置k1的过期时间为10秒(10秒后自动销毁)
(integer) 1 # 设置成功
127.0.0.1:6379[8]> get k1 # 获取k1
"v1"
127.0.0.1:6379[8]> ttl k1 # 查看k1的过期时间
(integer) 2 # 还有2秒过期
127.0.0.1:6379[8]> get k1
(nil)
127.0.0.1:6379[8]> keys * # 从内存中销毁了
(empty list or set)
type key
:查看键的数据类型127.0.0.1:6379[8]> type k1
string # k1的数据类型是会string字符串
操作文档:http://redisdoc.com/
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
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"
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"
127.0.0.1:6379> setex k1 5 v1 # 添加k1 v1数据的同时,设置5秒的声明周期
OK
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379> get k1
(nil) # 已过期,k1的值v1自动销毁
127.0.0.1:6379> setnx k1 sun
(integer) 0 # 添加失败,因为k1已经存在
127.0.0.1:6379> get k1
"laosun"
127.0.0.1:6379> setnx k2 sun
(integer) 1 # k2不存在,所以添加成功
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
127.0.0.1:6379> getset k6 v6
(nil) # 因为没有k6,所以get为null,然后将k6v6的值添加到数据库
127.0.0.1:6379> keys *
1) "k4"
2) "k1"
3) "k2"
4) "k3"
5) "k5"
6) "k6"
127.0.0.1:6379> get k6
"v6"
127.0.0.1:6379> getset k6 vv6 # 先获取k6的值,然后修改k6的值为vv6
"v6"
127.0.0.1:6379> get k6
"vv6"
push 和 pop,类似机枪 AK47:push,压子弹,pop,射击出子弹
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"
127.0.0.1:6379> lpop list02 # 从左(上)边移除第一个元素
"1"
127.0.0.1:6379> rpop list02 # 从右(下)边移除第一个元素
"5"
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"
127.0.0.1:6379> llen list01
(integer) 5
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"
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"
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"
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"
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"
性能总结:类似添加火车皮一样,头尾操作效率高,中间操作效率惨;
和 Java中的 Set 特点类似,不允许重复
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 # 不存在
127.0.0.1:6379> scard set01
(integer) 3 # 集合中有3个元素
127.0.0.1:6379> srem set01 2 # 移除set01中的元素2
(integer) 1 # 1表示移除成功
127.0.0.1:6379> sadd set01 1 2 3 4 5 6 7 8 9
(integer) 9
127.0.0.1:6379> smembers set01
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
7) "7"
8) "8"
9) "9"
127.0.0.1:6379> srandmember set01 3 # 从set01中随机获取3个元素
1) "8"
2) "2"
3) "3"
127.0.0.1:6379> srandmember set01 5 # 从set01中随机获取5个元素
1) "5"
2) "8"
3) "7"
4) "4"
5) "6"
127.0.0.1:6379> smembers set011) "1"2) "2"3) "3"4) "4"5) "5"6) "6"7) "7"8) "8"9) "9"127.0.0.1:6379> spop set01 # 随机移除一个元素"8"127.0.0.1:6379> spop set01 # 随机移除一个元素"7"127.0.0.1:6379> smembers set011) "1"2) "2"3) "3"4) "4"5) "5"6) "6"7) "9"
127.0.0.1:6379> sadd set01 1 2 3 4 5(integer) 5127.0.0.1:6379> sadd set02 x y z(integer) 3127.0.0.1:6379> smove set01 set02 3 # 将set01中的元素3移动到set02中(integer) 1 # 移动成功127.0.0.1:6379> smembers set011) "1"2) "2"3) "4"4) "5"127.0.0.1:6379> smembers set021) "z"2) "y"3) "3"4) "x"
127.0.0.1:6379> sadd set01 1 2 3 4 5
(integer) 5
127.0.0.1:6379> sadd set02 2 a 1 b 3
(integer) 5
127.0.0.1:6379> sinter set01 set02 # set01和set02共同存在的元素
1) "1"
2) "2"
3) "3"
127.0.0.1:6379> sunion set01 set02 # 将set01和set02中所有元素合并起来(排除重复的)
1) "5"
2) "4"
3) "3"
4) "2"
5) "b"
6) "a"
7) "1"
127.0.0.1:6379> sdiff set01 set02 # 在set01中存在,在set02中不存在
1) "4"
2) "5"
127.0.0.1:6379> sdiff set02 set01 # 在set02中存在,在set01中不存在
1) "b"
2) "a"
类似 Java里面的 Map
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"
127.0.0.1:6379> hgetall student1) "id"2) "101"3) "name"4) "tom"127.0.0.1:6379> hlen student(integer) 2 # student属性的数量,id和name,共两个属性
127.0.0.1:6379> hexists student name # student中是否存在name属性
(integer) 1 # 存在
127.0.0.1:6379> hexists student age # student中是否存在age属性
(integer) 0 # 不存在
127.0.0.1:6379> hkeys student # 获取student所有的属性名
1) "id"
2) "name"
127.0.0.1:6379> hvals student # 获取student所有属性的值(内容)
1) "101"
2) "tom"
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"
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" # 可以添加中文,但是显示为乱码(后期解决)
真实需求:充10元可享vip1;充20元可享vip2;充30元可享vip3;以此类推…
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"
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"
127.0.0.1:6379> zrem zset01 vip5 # 移除vip5
(integer) 1
127.0.0.1:6379> zcard zset01 # 集合中元素的个数
(integer) 4
127.0.0.1:6379> zcount zset01 20 30 # 分数在20~30之间,共有几个元素
(integer) 2
127.0.0.1:6379> zrank zset01 vip3 # vip3在集合中的下标(从上向下)
(integer) 2
127.0.0.1:6379> zscore zset01 vip2 # 通过元素获得对应的分数
"20"
127.0.0.1:6379> zrevrank zset01 vip3
(integer) 1
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"
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)
默认的自动备份策略不利于我们测试,所以修改 redis.conf 文件中的自动备份策略
vim redis.conf
/SNAP # 搜索
save 900 1 # 900秒内,至少变更1次,才会自动备份
save 120 10 # 120秒内,至少变更10次,才会自动备份
save 60 10000 # 60秒内,至少变更10000次,才会自动备份
使用 shutdown 模拟关机 ,关机之前和关机之后,对比 dump.rdb 文件的更新时间
开机启动 Redis,我们要在 120 秒内保存 10 条数据,再查看 dump.rdb 文件的更新时间(开两个终端窗口,方便查看)
120 秒内保存 10 条数据这一动作触发了备份指令,目前,dump.rdb 文件中保存了 10 条数据,将 dump.rdb 拷贝一份 dump10.rdb,此时两个文件中都保存 10 条数据
既然有数据已经备份了,那我们就肆无忌惮的将数据全部删除 flushall,再次 shutdown 关机
再次启动 Redis,发现数据真的消失了,并没有按照我们所想的将 dump.rdb 文件中的内容恢复到 Redis中。为什么?
将 dump.rdb 文件删除,将 dump10.rdb 重命名为 dump.rdb
启动 Redis 服务,登录 Redis,数据 10 条,全部恢复!
优势 / 劣势
为了避免失误,最好将 redis.conf 总配置文件备份一下,然后再修改内容如下:
appendonly yes
appendfilename appendonly.aof
重新启动 redis,以新配置文件启动
redis-server /usr/local/redis5.0.4/redis.conf
连接 redis,加数据,删库,退出
查看当前文件夹多一个 aof 文件,看看文件中的内容,保存的都是写操作
只需要重新连接,数据恢复成功
我们查看 redis.conf 文件,AOF 和 RDB 两种备份策略可以同时开启,那系统会怎样选择?
动手试试,编辑 appendonly.aof,胡搞乱码,保存退出
启动 redis 失败(写入代码格式不对),所以是 AOF 优先载入来恢复原始数据!因为 AOF 比 RDB 数据保存的完整性更高!
修复 AOF 文件,杀光不符合 redis 语法规范的代码
reids-check-aof --fix appendonly.aof
可以一次执行多个命令,是一个命令组,一个事务中,所有命令都会序列化(排队),不会被插队
一个队列中,一次性,顺序性,排他性的执行一系列命令
三特性
三步走
与关系型数据库事务相比
开启事务,加入队列,一起执行,并成功
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
放弃之前的操作,恢复到原来的值
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"
测试:模拟收入与支出
127.0.0.1:6379> set in 100 # 收入100元
OK
127.0.0.1:6379> set out 0 # 支出0元
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby in 20 # 收入-20
QUEUED
127.0.0.1:6379> incrby out 20 # 支出+20
QUEUED
127.0.0.1:6379> exec
1) (integer) 80
2) (integer) 20 # 结果,没问题!
127.0.0.1:6379> watch in # 监控收入in
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby in 20
QUEUED
127.0.0.1:6379> incrby out 20
QUEUED
127.0.0.1:6379> exec
(nil) # 在exec之前,我开启了另一个窗口(线程),对监控的in做了修改,所以本次的事务将被打断(失效),类似于“乐观锁”
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.conf
bind 0.0.0.0 #允许任何ip访问 重启redis: redis-server /opt/redis5.0.4/redis.conf
启动三台 redis,并查看每台机器的角色,都是 master
info replication
测试开始
首先,将三个机器全都清空,第一台添加值
mset k1 v1 k2 v2
其余两台机器,复制(找大哥)
slaveof 192.168.204.141 6379
第一台再添加值
set k3 v3
思考:slave 之前的 k1 和 k2 是否能拿到?
思考:slave 之后的 k3 是否能拿到?
思考:同时添加 k4,结果如何?
思考:主机 shutdown,从机如何?
思考:主机重启,从机又如何?
思考:从机死了,主机如何?从机归来身份是否变化?
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
slaveof no one # 2上执行,没有人能让我臣服,那我就是老大
slaveof 192.168.204.142 6379 # 3跟随2号
完成上面几个步骤后就完成了从服务器数据初始化的所有操作,从服务器此时可以接收来自用户的读请求
全量复制:Slave 初始化阶段,这时 Slave 需要将 Master 上的所有数据都复制一份 slave 接收到数据文件后,存盘,并加载到内存中(步骤 1234)
增量复制:Slave 初始化后,开始正常工作时主服务器发生的写操作同步到从服务器的过程(步骤 56)
Redis 主从同步策略:主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步
当然,如果有需要,slave 在任何时候都可以发起全量同步
redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步(shutdown再重新认master)
自动版的谋权篡位
只要发现主机挂了,从机们就会内部投票,选出新的主机
Sentinel 是 Redis 的高可用性解决方案:
模拟测试(1主,2和3从)
每一台服务器中创建一个配置文件 sentinel.conf,名字绝不能错,并编辑 sentinel.conf
cd /usr/local/bin
vi sentinel.conf
# sentinel monitor 被监控主机名(自定义) ip port 票数
sentinel monitor redis141 192.168.204.141 6379 1
sentinel monitor redis142 192.168.204.142 6379 1
sentinel monitor redis143 192.168.204.143 6379 1
启动服务顺序:主 Redis(141) --> 从 Redis(142 143)(执行slaveof 192.168.204.141 6379
认141为master) --> Sentinel1/2/3
# 新开三个窗口
cd /usr/local/bin
# 启动哨兵
redis-sentinel sentinel.conf
将 1 号老大挂掉,后台自动发起激烈的投票,选出新的老大
127.0.0.1:6379> shutdown
not connected> exit
查看最后权利的分配
如果之前的老大再次归来呢?
Java 和 Redis 打交道的 API 客户端
Java => MySQL:JDBC
Java => Redis:Jedis
导入依赖:
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
<version>3.1.0version>
dependency>
private void Test1(){
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.204.141",6379);
String pong = jedis.ping();
System.out.println("pong = " + pong);
}
}
// 运行前:
// 1.关闭防火墙 systemctl stop firewalld.service
// 2.修改redis.conf [ bind 0.0.0.0 ] 允许任何ip访问,以这个redis.conf启动redis服务
// 3.重启redis: redis-server /opt/redis-5.0.4/redis.conf
public class Test2_API {
private void testString(){
Jedis jedis = new Jedis("192.168.204.141",6379);
// string
jedis.set("k1","v1");
jedis.set("k2","v2");
jedis.set("k3","v3");
Set<String> set = jedis.keys("*");
Iterator<String> iterator = set.iterator();
for (set.iterator();iterator.hasNext();){
String k = iterator.next();
System.out.println(k+"->"+jedis.get(k));
}
Boolean k2Exists = jedis.exists("k2"); // 查看k2是否存在
System.out.println("k2Exists = " + k2Exists);
System.out.println( jedis.ttl("k1") );// 查看k1的过期时间
jedis.mset("k4","v4","k5","v5");
System.out.println( jedis.mget("k1","k2","k3","k4","k5") );
System.out.println("--------------------------------------------------------");
}
private void testList(){
Jedis jedis = new Jedis("192.168.204.141",6379);
// list
jedis.lpush("list01", "l1","l2","l3","l4","l5");
List<String> list01 = jedis.lrange("list01", 0, -1);
for(String s : list01){
System.out.println(s);
}
System.out.println("--------------------------------------------------------");
}
private void testSet(){
Jedis jedis = new Jedis("192.168.204.141",6379);
// set
jedis.sadd("order","jd001");
jedis.sadd("order","jd002");
jedis.sadd("order","jd003");
Set<String> order = jedis.smembers("order");
Iterator<String> order_iterator = order.iterator();
while(order_iterator.hasNext()){
String s = order_iterator.next();
System.out.println(s);
}
jedis.srem("order", "jd002");
System.out.println( jedis.smembers("order").size() );
}
private void testHash(){
Jedis jedis = new Jedis("192.168.204.141",6379);
jedis.hset("user1", "username","james");
System.out.println( jedis.hget("user1", "username") );
HashMap<String, String> map = new HashMap<String, String>();
map.put("username", "tom");
map.put("gender", "boy");
map.put("address", "beijing");
map.put("phone", "13590875543");
jedis.hmset("user2", map);
List<String> list = jedis.hmget("user2", "username", "phone");
for(String s: list){
System.out.println(s);
}
}
private void testZset(){
Jedis jedis = new Jedis("192.168.204.141",6379);
jedis.zadd("zset01", 60d, "zs1");
jedis.zadd("zset01", 70d, "zs2");
jedis.zadd("zset01", 80d, "zs3");
jedis.zadd("zset01", 90d, "zs4");
Set<String> zset01 = jedis.zrange("zset01", 0, -1);
Iterator<String> iterator = zset01.iterator();
while (iterator.hasNext()){
String s = iterator.next();
System.out.println(s);
}
}
public static void main(String[] args) {
new Test2_API().testZset();
}
}
初始化余额和支出
127.0.0.1:6379> set yue 100
127.0.0.1:6379> set zhichu 0
public class Test_Transaction {
public static void main(String[] args) throws Exception {
Jedis jedis = new Jedis("192.168.204.141",6379);
int yue = Integer.parseInt( jedis.get("yue") );
int zhichu = 10;
jedis.watch("yue"); // 监控余额
Thread.sleep(5000); // 模拟网络延迟
if(yue < zhichu) {
jedis.unwatch(); //解除监控
System.out.println("余额不足!");
}else {
Transaction transaction = jedis.multi(); // 开启事务
transaction.decrBy("yue", zhichu); // 余额减少
transaction.incrBy("zhichu", zhichu); // 累计消费增加
transaction.exec();
System.out.println("余额:" + jedis.get("yue"));
System.out.println("累计支出:" + jedis.get("zhichu"));
}
}
}
<dependency> <groupId>commons-poolgroupId> <artifactId>commons-poolartifactId> <version>1.6version>dependency>
public class JedisPoolUtil {
private JedisPoolUtil(){}
private volatile static JedisPool jedisPool = null; // 相当于水池
private volatile static Jedis jedis = null; // 相当于水池里的鱼
// 返回一个连接池 本类调用
private static JedisPool getInstance() {
// 双层检测锁(企业中用的非常频繁)
if(jedisPool == null) { // 第一层:检测体温
synchronized (JedisPoolUtil.class) { // 排队进站
if(jedisPool == null) { //第二层:查看健康码
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(1000); // 资源池中的最大连接数
config.setMaxIdle(30); // 资源池允许的最大空闲连接数
config.setMaxWaitMillis(60*1000); // 当资源池连接用尽后,调用者的最大等待时间(单位为毫秒)
config.setTestOnBorrow(true); //向资源池借用连接时是否做连接有效性检测(业务量很大时候建议设置为false,减少一次ping的开销)
jedisPool = new JedisPool( config, "192.168.204.141", 6379);
}
}
}
return jedisPool;
}
// 返回jedis对象 提供给所有人使用
public static Jedis getJedis() {
if(jedis == null) {
jedis = getInstance().getResource();
}
return jedis;
}
}
public class Test_JedisPool {
public static void main(String[] args) {
Jedis jedis1 = JedisPoolUtil.getJedis();
Jedis jedis2 = JedisPoolUtil.getJedis();
System.out.println(jedis1==jedis2); // true
}
}
<packaging>warpackaging>
<dependencies>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-webmvcartifactId>
<version>5.2.7.RELEASEversion>
dependency>
<dependency>
<groupId>org.redissongroupId>
<artifactId>redissonartifactId>
<version>3.6.1version>
dependency>
<dependency>
<groupId>org.springframework.datagroupId>
<artifactId>spring-data-redisartifactId>
<version>2.3.2.RELEASEversion>
dependency>
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
<version>3.1.0version>
dependency>
<dependency>
<groupId>com.fasterxml.jackson.coregroupId>
<artifactId>jackson-databindartifactId>
<version>2.9.8version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.mavengroupId>
<artifactId>tomcat7-maven-pluginartifactId>
<configuration>
<port>8001port>
<path>/path>
configuration>
<executions>
<execution>
<phase>packagephase>
<goals>
<goal>rungoal>
goals>
execution>
executions>
plugin>
plugins>
build>
<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>springmvcservlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
<init-param>
<param-name>contextConfigLocationparam-name>
<param-value>classpath:spring/spring.xmlparam-value>
init-param>
servlet>
<servlet-mapping>
<servlet-name>springmvcservlet-name>
<url-pattern>/url-pattern>
servlet-mapping>
web-app>
<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>
cd /usr/local/bin
redis-server /opt/redis-5.0.4/redis.conf
redis-cli
127.0.0.1:6379> set phone 10
@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!";
}
}
vi /usr/local/nginx/conf/nginx.conf
upstream sga{
#本机IP地址 不是虚拟机的
server 192.168.204.1:8001;
server 192.168.204.1:8002;
}
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
proxy_pass http://sga;
root html;
index index.html index.htm;
}
}
# 启动nginx -c: 以nginx.conf配置文件启动
/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
# 关闭防火墙
systemctl stop firewalld.service
使用 JMeter 模拟 1 秒内发出 100 个 http 请求,会发现同一个商品会被两台服务器同时抢购!
synchronized锁的一个进程下的线程并发,如果分布式环境,多个进程并发,这种方案就失效了
自己实现分布式锁,太难了!
@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”