目录标题)
一、主从复制原理(读写分离)
原理:第一次同步数据由从服务器发起,当主服务器接收到从服务器要求同数据的请求后,将dump.rdb文件发送给从服务器。之后主服务器每次写操作后,由主服务器发起同步请求。
1、配置一主二从集群
- 创建文件夹:
# 6379
[root@iz2zedg4ylq9iqtwm11wecz redis6379]# pwd
/my/redises/redis6379
[root@iz2zedg4ylq9iqtwm11wecz redis6379]# ls
conf data
#6380
[root@iz2zedg4ylq9iqtwm11wecz redis6380]# pwd
/my/redises/redis6380
[root@iz2zedg4ylq9iqtwm11wecz redis6380]# ls
conf data
[root@iz2zedg4ylq9iqtwm11wecz redis6380]#
#6381
[root@iz2zedg4ylq9iqtwm11wecz redis6381]# pwd
/my/redises/redis6381
[root@iz2zedg4ylq9iqtwm11wecz redis6381]# ls
conf data
[root@iz2zedg4ylq9iqtwm11wecz redis6381]#
- 复制redis.conf
将redis.conf分别复制到三个目录中,并将redis.conf其中的端口号修改为6379、6380、6381
[root@iz2zedg4ylq9iqtwm11wecz conf]# pwd
/my/redises/redis6379/conf
[root@iz2zedg4ylq9iqtwm11wecz conf]# cp /my/redis/conf/redis.conf ./
- 使用docker开启三个reids容器
#6379
[root@iz2zedg4ylq9iqtwm11wecz ~]# docker run --name redis6379 -p 6379:6379 --net host -v /my/redises/redis6379/conf/redis.conf:/usr/local/etc/redis/redis.conf -v /my/redises/redis6379/data/:/data -d redis redis-server /usr/local/etc/redis/redis.conf
3c3d9c85cc22ca483eda0da1ead04e7dd58fd0b74438ad4529855f7cd284bb5a
[root@iz2zedg4ylq9iqtwm11wecz ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
[root@iz2zedg4ylq9iqtwm11wecz ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS
3c3d9c85cc22 redis "docker-entrypoint..." 26 seconds ago Exited (0) 25 sec
dfabf4da2bdd nginx "/docker-entrypoin..." 2 months ago Exited (255) 7 we
598fef497cec redis "docker-entrypoint..." 2 months ago Exited (0) 11 min
78c4a571ae3b mysql:5.7 "docker-entrypoint..." 2 months ago Exited (255) 7 we
[root@iz2zedg4ylq9iqtwm11wecz ~]#
#6380
[root@iz2zedg4ylq9iqtwm11wecz ~]# docker run --name redis6380 -p 6380:6380 --net host -v /my/redises/redis6380/conf/redis.conf:/usr/local/etc/redis/redis.conf -v /my/redises/redis6380/data/:/data -d redis redis-server /usr/local/etc/redis/redis.conf
996d60edaa59d7485920a355e25a362a7b51e0c38e8cd13e92b7b72a68c1b74d
#6381
[root@iz2zedg4ylq9iqtwm11wecz ~]# docker run --name redis6381 -p 6381:6381 --net host -v /my/redises/redis6381/conf/redis.conf:/usr/local/etc/redis/redis.conf -v /my/redises/redis6381/data/:/data -d redis redis-server /usr/local/etc/redis/redis.conf
aab7ed4ad32f6f4944aa95dda085e958a0d342302fa8c73f5886e52734f2e40c
- 通过docker start 容器名 启动三个redis容器
[root@iz2zedg4ylq9iqtwm11wecz ~]# docker start redis6379
redis6379
[root@iz2zedg4ylq9iqtwm11wecz ~]# docker start redis6380
redis6380
[root@iz2zedg4ylq9iqtwm11wecz ~]# docker start redis6381
redis6381
[root@iz2zedg4ylq9iqtwm11wecz ~]#
- 进入容器
docker exec -it redis6379 /bin/bash
docker exec -it redis6380 /bin/bash
docker exec -it redis6381 /bin/bash
- 通过slaveof ip port 将这个redis所在的主机变为从机
注意:修改了redis的端口号后,在打开redis-cli时需要指定对应的端口号,比如redis-cli -p 6380
# 6380( 这里显示6379是因为dockaer端口映射的原理,这台主机外界需要通过6380这个端口进行访问)
127.0.0.1:6379> slaveof 39.96.52.225 6379
OK
# 6381
127.0.0.1:6379> slaveof 39.96.52.225 6379
OK
- 通过命令info replication 查看redis的信息
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=39.96.52.225,port=6379,state=online,offset=70,lag=0
slave1:ip=39.96.52.225,port=6379,state=online,offset=70,lag=1
master_failover_state:no-failover
master_replid:57e856a00e5642b200b23c9872fa7f0cad475381
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:70
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:70
127.0.0.1:6379>
注意:如果失败,原因如下:
1、没有开放端口,包括阿里云
2、redis.conf配置里面没有关闭保护模式和只允许本机访问
2、测试读写分离
注意主机redis能进行读和写,但是从机redis只能就行读,不能写
# 在主机设置一个key,在两个从机中也能获取到响应的key
# 6379
127.0.0.1:6379> hset user id 1 name lihua age 18
(integer) 3
# 6380
127.0.0.1:6379> hgetall user
1) "id"
2) "1"
3) "name"
4) "lihua"
5) "age"
6) "18"
# 6381
127.0.0.1:6379> hgetall user
1) "id"
2) "1"
3) "name"
4) "lihua"
5) "age"
6) "18"
尝试在从机写一些key
127.0.0.1:6379> set 1 1
(error) READONLY You can't write against a read only replica.
3、服务器宕机后
- 当从机宕机后,再次启动时
从服务器不会直接成为主服务器的从服务器,需要再次对从服务器进行加入(slaveof ip port)操作。加入后会从主服务器中从头到尾重新同步数据,并再次成为主服务器的从服务器。 - 当主服务器宕机后。
当主服务器恢复后从服务器还是从服务器。但是主服务器宕机期间,从服务器还是只能有读操作,没有写操作。
二、主从复制——薪火相传(链式的)
薪火相传也就是,从服务器也能有从服务器。从服务器也可以通过slaveof ip port 添加从服务器
三、反客为主 (主服务器宕机后从服务器变为主服务器)
可以通过命令slaveof no one 将从服务器变为主服务器
注意:
执行slaveof on one命令后,从服务器会变为主服务器。但是这个服务器不在是之前主服务器的从服务器。也就是说这个服务器,将变成主服务器,但是此时它是没有任何的从服务器,也没有成为任何主机的从服务器。
四、哨兵模式(升级版反客为主)
1、创建sentinel.conf文件
在redis容器中创建sentinel.conf文件
root@0bee7c2d98cb:/usr/local/bin# pwd
/usr/local/bin
root@0bee7c2d98cb:/usr/local/bin# touch sentinel.conf
ok
root@0bee7c2d98cb:/usr/local/bin# ls
docker-entrypoint.sh redis-benchmark redis-check-rdb redis-sentinel sentinel.conf
gosu redis-check-aof redis-cli redis-server
编写sentinel.conf文件,如果没有vim、vi那么可以在宿主主机中用docker cp将编写好的文件复制到容器中。
sentinel monitor mymastter 39.96.52.225 6379 1
# mymastter:给主机一个名称; 39.96.52.225 6379:主机的IP和端口号,1:表示多少个哨兵同意就能将从机升为主机
也可以这样写入
# 写入
echo "sentinel monitor mymastter 39.96.52.225 6379 1" > sentinel.conf
# 查看
cat sentinel.conf
2、启动哨兵
注意:1、在redis的安装目录启动(也就是有redis-sentinel 文件下)
2、注意不要在主机的容器中启动哨兵,因为等下就是要关闭主机,主机的容器关闭后,哨兵也会被关闭,这样就无法监听了
root@0bee7c2d98cb:/usr/local/bin# pwd
/usr/local/bin
root@0bee7c2d98cb:/usr/local/bin# ls
docker-entrypoint.sh redis-benchmark redis-check-rdb redis-sentinel sentinel.conf
gosu redis-check-aof redis-cli redis-server
root@0bee7c2d98cb:/usr/local/bin#
# 运行
redis-sentinel sentinel.conf
3、然后通过docker stop 关闭主reids
docker stop redis6379
主机被关闭后哨兵就会自动选择一台从机作为主机,并且之前的主机会变为现在主机的从机。当之前的主机重新运行时依旧会作为新主机的从机
当从机变为主机后,会将以前主机拥有的从机变为自己的从机,以前的主机也会变成自己的从机
4、主机挂掉后如何选举主机呢?
- 首先根据优先级 在reids.conf 中配置 replica-priority 100
值越小优先级越高 - 当优先级一样,选择从主机中同步到的数据最全的(看key的个数)
- 根据redis运行后随机生成的 的runid,越小越优先
五、搭建redis集群
1、并发写操作(之前的主从复制,只能有一台主机有写功能。搭建redis集群后可以实现多台服务器有写功能。)
2、统一调用接口(并且之前的服务器必须通过不同的ip和端口号进行访问。不能进行统一ip端口进行访问。)
3、rdis集群实现了内存扩容
1、修改redis.conf文件
# 修改redis.conf文件
[root@iz2zedg4ylq9iqtwm11wecz conf]# pwd
/my/redises/redis6379/conf
[root@iz2zedg4ylq9iqtwm11wecz conf]# ls
redis.conf
[root@iz2zedg4ylq9iqtwm11wecz conf]# vim redis.conf
cluster-enabled yes #开始集群
#集群节点的配置文件名,注意这个配置文件不是启动redis的配置文件,而是在运行后自动生成的集群节点的配置文件
cluster-config-file nodes-6379.conf
cluster-node-timeout 15000 #改节点的超时时间,超时自动切换节点
2、修改redis的默认端口号
在redis.conf 里面就行修改就可以了
# 运行redis客户端
redis-cli -p 6380
注意修改节点的端口号后在连接redis-cli时要指定端口号
3、运行redis后nodes-6379.conf 会在工作目录生成
root@74558bbeeb6e:/data# ls
dump.rdb nodes-6379.conf
root@e763b91a444c:/data# ls
dump.rdb nodes-6380.conf
root@35adf3a37c72:/data# ls
dump.rdb nodes-6381.conf
注意:请确保节点配置文件都生成
4、将节点合成集群
使用docker安装redis,在运行容器时指定的安装目录为:
root@0bee7c2d98cb:/usr/local/bin# pwd
/usr/local/bin
创建集群
# 在改目录下运行
root@74558bbeeb6e:/usr/local/bin# pwd
/usr/local/bin
root@74558bbeeb6e:/usr/local/bin# redis-cli --cluster create 39.96.52.225:6379 39.96.52.225:6380 39.96.52.225:6381
注意:如果运行失败注意redis的版本
失败参考
我的redis版本为:
root@74558bbeeb6e:/usr/local/bin# redis-server --version redis-server -v
Redis server v=6.2.3 sha=00000000:0 malloc=jemalloc-5.1.0 bits=64 build=dc20d908b7b619b4
生成集群时可能会报以下错误
[ERR] Node 39.96.52.225:6379 is not empty. Either the node already knows other nodes (check with CLUSTER NODES) or contains some key in database 0.
成功如下
如果一直等待加入,可能需要修改
1、开放端口号,如果节点的端口为6379 ,那么需要开放6379-16379。比如节点中最大的端口为6389,那么要开放6379-16389
参考
2、如果使用docker搭建的redis,需要在创建docker容器创建时允许联网
解决:创建容器时添加–net host 即可如下
docker run --name redis6379 -p 6379:6379 --net host -v /my/redises/redis6379/conf/redis.conf:/usr/local/etc/redis/redis.conf -v /my/redises/redis6379/data/:/data -d redis redis-server /usr/local/etc/redis/redis.conf
5、连接redis集群
注意:通过任意端口都能连接集群,连接集群后能操作集群里面的所有节点
# 使用 redis-cli -c -p 6379 连接集群,-c 表示连接集群 -p 表示端口号,任意节点的端口号都可以
root@iz2zedg4ylq9iqtwm11wecz:/data# redis-cli -c -p 6379
127.0.0.1:6379>
# 当然你也可以使用redis-cli 连接单个节点,这样就没有连接上集群
root@iz2zedg4ylq9iqtwm11wecz:/data# redis-cli -p 6380
127.0.0.1:6380>
# 查看集群的节点
127.0.0.1:6379> cluster nodes
098bd49903e178ddcca36222b7227602c9c5d63e 39.96.52.225:6380@16380 master - 0 1629266807819 2 connected 5461-10922
c75a4531edba630b7445a70b5f48a89fc8e64d3c 39.96.52.225:6381@16381 master - 0 1629266807000 3 connected 10923-16383
a3b57cd2eeb9d7b049d76668c68e4394e96f6164 172.24.12.28:6379@16379 myself,master - 0 1629266803000 1 connected 0-5460
在set key时集群会自动根据key,分配给各个节点进行存储
127.0.0.1:6379> set 11 11
OK
127.0.0.1:6379> set 1122 11
-> Redirected to slot [7470] located at 39.96.52.225:6380
OK
39.96.52.225:6380> set 22 11
-> Redirected to slot [13798] located at 39.96.52.225:6381
OK
39.96.52.225:6381>
6、集群中如何分配节点(那些节点成为主节点,那些节点为从节点,从节点怎么认主)
原则:分配是保证每个主节点运行在不同的ip(服务器)上,主节点和从节点不在同一个ip(服务器上)。目的是防止一台服务器挂掉后,集群受到的影响最小。
7、如何存储key(负载均衡)
根据key的值,利用算法,计算出一个范围在0——16384的slot值,再根据这个slot值确定由哪个节点存储
8、redis集群常见操作
# 根据k2计算出slot,根据slot 的值确定将这个值存到6379这个节点
39.96.52.225:6381> set k2 v1
-> Redirected to slot [449] located at 39.96.52.225:6379
OK
39.96.52.225:6379> get 11
"11"
# 根据k1计算出slot,根据slot 的值找到key在6379这个节点里
39.96.52.225:6379> get k1
-> Redirected to slot [12706] located at 39.96.52.225:6381
"v1"
# 计算key的slot值
39.96.52.225:6381> cluster keyslot k1
(integer) 12706
9、redis集群存在的一些问题
- mset 指令与单节点不同
# 使用单节点的指令会报错
39.96.52.225:6381> mset 1 1 2 2 3 3 4 4
(error) CROSSSLOT Keys in request don't hash to the same slot
原因:因为这里的指令mset有多个key,redis的负载均衡算法无法计算出来这些key的范围
解决:给这些key分组
39.96.52.225:6381> mset 1{num} 1 2{num} 2 3{num} 3 4{num} 4
-> Redirected to slot [2765] located at 39.96.52.225:6379
OK
10、故障恢复
主节点宕机后,有两种情况
1、若有从节点,那么从节点会自动升级变为主节点,当以前的主节点恢复后,会成为新主节点的从节点。
2、若没有从节点(或者主节点和从节点都宕机),那么redis的默认配置是,整个集群都不可用。可以通过修改redis.conf的参数配置 cluster-require-full-coverage yes 进行配置 。yes为整个集群不可用。
11、jedis使用redis集群
/**
* reis集群测试
* @author 15594
*/
public class RedisCluster {
public static void main(String[] args) {
//创建集群操作对象
//端口号可以时集群节点中的任意一个
HostAndPort hostAndPort = new HostAndPort("39.96.52.225", 6381);
JedisCluster jedisCluster = new JedisCluster(hostAndPort);
jedisCluster.set("b1","value1");
String b1 = jedisCluster.get("b1");
System.out.println(b1);
}
}
六、redis应用问题
1、缓存穿透——访问不存在的key
请求获取数据的原则是,先访问redis缓存,redis中不存在所需要的数据,就去访问数据库。
- 什么是缓存穿透
当非正常的请求(url)恶意访问redis中不存在的key,使得频繁访问数据库。 - 解决方案:
- 对空值就行缓存。在查询数据库后,对不存在的key也加入redis缓存,但是要设置过期时间。
- 设置白名单。限制ip访问,或则限制访问次数。
- 如果redis缓存命中率降低,那么就是核查
可以通过布隆过滤器解决缓存穿透问题。
布隆过滤器详解 看了就会用
2、缓存击穿——某个热门的key过期了
- 什么是缓存击穿
瞬时大量请求访问到某个过期的key,然后这些请求瞬时访问数据库,照成数据库崩溃。 - 解决方案
- 预先设置热门数据
- 根据访问量增加过期时间
3、缓存雪崩——访问到大量过期的key
- 什么是缓存雪崩
访问到大量过期的key,造成大量的请求访问到数据库,造成服务雪崩 - 解决方案
- 使用多级缓存架构 (nginx缓存+redis缓存+其他缓存)
- 使用锁或者队列(当大量key失效时,对需要直接访问数据库的请求进行加锁或者加入队列,减轻数据库访问压力)
- 设置过期标志更新缓存。(未雨绸缪,提前预知key的过期时间,提前更新key)
- 分散key的过期时间。(不要集中设置key的过期时间)
七、分布式锁
与单机的锁不一样,分布式的锁,对不同的主机是不可见的。因为你不能跨操作系统。单机的锁由jvm控制。
分布式锁是解决分布式环境下集群对共享资源的操作。
-
不加锁
-
加synchronized、lock锁
3. 当系统升级变为分布式的集群
- 解决分布式集群操作共享资源问题——使用分布式锁
1、redis如何实现
- 怎么上锁
# setnx——当key不存在时才能添加key
127.0.0.1:6379> setnx 2 2
(integer) 1
- 怎么解锁
# 删除这个key,其他请求才能setnx 这个key(上锁)
127.0.0.1:6379> del 2
(integer) 1
- 只是上锁可能会形成死锁
解决:添加超时时间。超时就自动释放锁,也就是给key设置一个过期时间
# nx——不存在时才添加key ,px设置过期时间 以毫秒为单位 ex是以秒为单位
127.0.0.1:6379> set 3 3 nx px 5000
OK
注意:这里不能通过先setnx key value,在给key设置一个过期时间。因为这样分两条指令执行,在分布式中可能会被打断,照成无法成功设置过期时间
2、jedis实现
2.1、不使用lua脚本(在进行if判断时可能会被自动过期指令打断)
package com.lihua.springbootredis.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* 分布式锁——redis实现
* @author 15594
*/
@RestController
public class ReidLockController {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping("/testLock")
public void testLock(){
//分布式中一般用雪花算法获取id
UUID uuid = UUID.randomUUID();
String uid = uuid.toString();
System.out.println(uid);
//1、获取锁,相当于setnx,如果能设置成功,返回true,成功获取锁,否则失败 3:过期时间值 TimeUnit.SECONDS 时间单位
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uid,3, TimeUnit.SECONDS);
//2、如果获取到锁了,就能操作共享资源
if (lock){
Integer num = (Integer)redisTemplate.opsForValue().get("num");
if (num==null){
System.out.println("num为null");
return;
}
if (num==0){
return;
}
String isMyLock= (String) redisTemplate.opsForValue().get("lock");
System.out.println(isMyLock);
//进行--是也要判断自己的锁是否已近过期被释放并且被其他客户端获取,//isMyLock==null&&isMyLock.equals(uid) 防止代码执行到这里key过期了
if (num>0&&isMyLock!=null&&isMyLock.equals(uid)){
System.out.println("num--");
//这里依旧会出问题,因为key可能在进行了if判断后过期,所有要通过lua脚本保证判断和修改同时就行,不会被打断(自动过期的指令打断)
redisTemplate.opsForValue().decrement("num");
}
//防止释放错别人的锁(锁key过期后,自动释放锁,锁又被其他请求上锁,如果不去判断是不是自己的锁就会误释放别人的锁)
if(redisTemplate.opsForValue().get("lock").equals(uid)){
//这里依旧会出问题,因为key可能在进行了if判断后过期,所有要通过lua脚本保证判断和修改同时就行,不会被打断(自动过期的指令打断)
System.out.println("释放锁");
redisTemplate.delete("lock");
}
System.out.println(redisTemplate.opsForValue().get("num"));
}else {
//3、获取锁失败等待100毫秒再次尝试获取锁
try {
Thread.sleep(100);
testLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
2.2、使用lua脚本(保证了判断与执行的原子性)
package com.lihua.springbootredis.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* 分布式锁——redis实现
* @author 15594
*/
@RestController
public class ReidLockController {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping("/testLockLua")
public void testLockLua(){
//分布式中一般用雪花算法获取id
UUID uuid = UUID.randomUUID();
String uid = uuid.toString();
System.out.println(uid);
//1、获取锁,相当于setnx,如果能设置成功,返回true,成功获取锁,否则失败 3:过期时间值 TimeUnit.SECONDS 时间单位
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uid,3, TimeUnit.SECONDS);
//2、如果获取到锁了,就能操作共享资源
if (lock){
Integer num = (Integer)redisTemplate.opsForValue().get("num");
if (num==null){
return;
}
if (num==0){
return;
}
String script = "local i=redis.call('get','num')\n" +
"\n" +
"if tonumber(i)<=0 then \n" +
" return 0;\n" +
"end\n" +
"if redis.call('exists',KEYS[1])==0 then \n" +
" return 1;\n" +
"end\n" +
"if redis.call('get',KEYS[1])==ARGV[1] then\n" +
" redis.call('decr','num');\n" +
"end\n" +
"return 2;";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
//设置lua脚本返回值
redisScript.setResultType(Long.class);
redisScript.setScriptText(script);
// 执行脚本 KEYS ARGV
redisTemplate.execute(redisScript, Arrays.asList("lock"), uid);
System.out.println(redisTemplate.opsForValue().get("num"));
String script1 = "if redis.call('exists',KEYS[1])==0 then\n" +
" return 0;\n" +
"end \n" +
"if redis.call('get',KEYS[1])==ARGV[1] then\n" +
" redis.call('del',KEYS[1])\n" +
" return 1;\n" +
"else\n" +
" return 0;\n" +
"end";
redisScript.setScriptText(script1);
redisTemplate.execute(redisScript,Arrays.asList("lock"),uid);
System.out.println("锁已近释放");
}else {
//3、获取锁失败等待100毫秒再次尝试获取锁
try {
Thread.sleep(100);
testLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
注意:在高并发环境下,if判断是不安全的。遇到if条件判断需要谨慎