Redis高可用

客户端分片集群

  • Jedis客户端分片访问

  • 一致性哈希算法

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

# 启动三个redis服务,使用 --net=host 方式,让容器直接使用主机的端口:6379,6380,6381
docker run -d --name redis6379 \
--net=host \
--restart=always \
redis

docker run -d --name redis6380 \
--net=host \
--restart=always \
redis redis-server --port 6380


docker run -d --name redis6381 \
--net=host \
--restart=always \
redis redis-server --port 6381

# 进入redis,测试数据操作
docker exec -it redis6379 redis-cli
set a aaaaaaaaaaaa
set b bbbbbbbb
keys *
get a 
get b
flushall

Jedis依赖:

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

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

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

// jedis分片API
// 服务器的地址列表
        List<JedisShardInfo> list = new ArrayList<>();
        list.add(new JedisShardInfo("192.168.64.140", 6379));
        list.add(new JedisShardInfo("192.168.64.140", 6380));
        list.add(new JedisShardInfo("192.168.64.140", 6381));
        // 连接池的配置对象
        GenericObjectPoolConfig cfg = new JedisPoolConfig();
        // 新建分片连接池
        ShardedJedisPool pool = new ShardedJedisPool(cfg, list);
        // 从连接池获取数据操作工具
        ShardedJedis j = pool.getResource();
        // 向集群添加100条数据
        for (int i = 0; i < 100; i++) {
            j.set("k"+i, "v"+i);
        }
        pool.close();

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown


# 进入服务器查看数据
docker exec -it redis6379 redis-cli
keys *

docker exec -it redis6380 redis-cli -p 6380
keys *

docker exec -it redis6381 redis-cli -p 6381
keys *

Jedis 分片客户端API,使用“一致性哈希”算法:

  • 环形结构

  • 2^32 -1个节点

  • 所有物理服务器均匀分散到哈希环上

  • 数据经过哈希运算落到一个节点上,从这个节点顺时针寻找物理服务器节点

主从+哨兵

主从集群

  • 存放到主服务器的数据,会向从服务器复制来作为备份

  • 如果主服务器宕机,可以访问从服务器的数据

  • 读写分离

    • 主服务器负责写入数据

    • 从服务器负责读取数据

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

# 清理容器
docker rm -f $(docker ps -aq) --volumes

# 启动主服务器
docker run -d --name redis6379 \
--net host \
--restart=always \
redis

# 启动两个从服务器
docker run -d --name redis6380 \
--net host \
--restart=always \
redis redis-server --port 6380 \
--slaveof 192.168.64.140 6379

docker run -d --name redis6381 \
--net host \
--restart=always \
redis redis-server --port 6381 \
--slaveof 192.168.64.140 6379

# 查看日志
docker logs redis6379

docker logs redis6380



# 查看三个服务器的角色
docker exec -it redis6379 redis-cli
info replication

docker exec -it redis6380 redis-cli -p 6380
info replication

docker exec -it redis6381 redis-cli -p 6381
info replication

# 向主服务器添加数据
docker exec -it redis6379 redis-cli
set a aaaaaa
exit

# 在从服务器查看是否有数据
docker exec -it redis6380 redis-cli -p 6380
get a
exit

# 主机宕机
docker stop redis6379

# 查看从机日志
docker logs -f redis6380

# 在从机访问数据
docker exec -it redis6380 redis-cli -p 6380
info replication
get a
set b bbbbbbbbbbbbbbbbb


# 手动将redis6380提升成主机
docker exec -it redis6380 redis-cli -p 6380
slaveof no one
info replication
exit

# 手动设置redis6381作为redis6380的从机
docker exec -it redis6381 redis-cli -p 6381
slaveof 192.168.64.140 6380
info replication
exit

# 启动6379后,也作为从机连接6380
docker restart redis6379
docker exec -it redis6379 redis-cli
slaveof 192.168.64.140 6380
info replication
exit

哨兵服务

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

# 准备三个哨兵的配置文件

mkdir /opt/sentinel/
cd /opt/sentinel/


cat <<EOF >5000.conf
port 5000
sentinel monitor mymaster 192.168.64.140 6380 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
sentinel parallel-syncs mymaster 1
EOF

cat <<EOF >5001.conf
port 5001
sentinel monitor mymaster 192.168.64.140 6380 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
sentinel parallel-syncs mymaster 1
EOF

cat <<EOF >5002.conf
port 5002
sentinel monitor mymaster 192.168.64.140 6380 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
sentinel parallel-syncs mymaster 1
EOF









# 启动三个哨兵服务
docker run -d --name sentinel5000 \
-v /opt/sentinel/5000.conf:/sentinel.conf \
--net host \
--restart=always \
redis redis-sentinel /sentinel.conf

docker run -d --name sentinel5001 \
-v /opt/sentinel/5001.conf:/sentinel.conf \
--net host \
--restart=always \
redis redis-sentinel /sentinel.conf

docker run -d --name sentinel5002 \
-v /opt/sentinel/5002.conf:/sentinel.conf \
--net host \
--restart=always \
redis redis-sentinel /sentinel.conf

# 通过哨兵查看集群信息
docker exec -it sentinel5000 redis-cli -p 5000
sentinel master mymaster
sentinel slaves mymaster
sentinel sentinels mymaster

# 测试主机宕机,哨兵自动执行主从切换
docker stop redis6380

# 查看哨兵日志
docker logs sentinel5000

# 重新启动6380
docker start redis6380

docker logs sentinel5000

Jedis客户端:

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

        // 哨兵服务器地址列表
        Set<String> set = new HashSet<>();
        set.add("192.168.64.140:5000");
        set.add("192.168.64.140:5001");
        set.add("192.168.64.140:5002");
        // 创建哨兵连接池
        JedisSentinelPool pool = new JedisSentinelPool("mymaster", set);
        // 从连接池取出数据操作工具对象
        Jedis j = pool.getResource();
        // 放100条数据
        for (int i = 0; i < 100; i++) {
            j.set("k" + i, "v" + i);
        }
        pool.close();

Redis Cluster集群

  • 可以是多主多从结构

  • 自带主从切换,不需要哨兵服务

  • 使用“哈希槽”算法

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

# 清理容器
docker rm -f $(docker ps -aq) --volumes

# 清理无主的数据卷
docker volume prune -f

# 准备配置文件
mkdir /opt/redis
cd /opt/redis
mkdir 7000 7001 7002 7003 7004 7005

cat <<EOF >7000/redis.conf
port 7000
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
EOF

cat <<EOF >7001/redis.conf
port 7001
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
EOF

cat <<EOF >7002/redis.conf
port 7002
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
EOF

cat <<EOF >7003/redis.conf
port 7003
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
EOF

cat <<EOF >7004/redis.conf
port 7004
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
EOF

cat <<EOF >7005/redis.conf
port 7005
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
EOF

# 启动6个redis服务
docker run -d --name redis7000 --restart=always \
--net host -v /opt/redis/7000/redis.conf:/redis.conf \
redis redis-server /redis.conf

docker run -d --name redis7001 --restart=always \
--net host -v /opt/redis/7001/redis.conf:/redis.conf \
redis redis-server /redis.conf

docker run -d --name redis7002 --restart=always \
--net host -v /opt/redis/7002/redis.conf:/redis.conf \
redis redis-server /redis.conf

docker run -d --name redis7003 --restart=always \
--net host -v /opt/redis/7003/redis.conf:/redis.conf \
redis redis-server /redis.conf

docker run -d --name redis7004 --restart=always \
--net host -v /opt/redis/7004/redis.conf:/redis.conf \
redis redis-server /redis.conf

docker run -d --name redis7005 --restart=always \
--net host -v /opt/redis/7005/redis.conf:/redis.conf \
redis redis-server /redis.conf

# 进入redis添加数据
# 组成集群之前,服务不能使用
docker exec -it redis7000 redis-cli -p 7000
set a aaaaaaaaaaa




# 组成集群
docker exec -it redis7000 redis-cli --cluster create \
192.168.64.140:7000 192.168.64.140:7001 \
192.168.64.140:7002 192.168.64.140:7003 \
192.168.64.140:7004 192.168.64.140:7005 \
--cluster-replicas 1

# 添加数据
docker exec -it redis7000 redis-cli -c -p 7000
set a aaaaaaaaaaaaaaaaaaaaaaaa
set b bbbbbbbbbbbbbbbbbbbbbbb
set c cccccccccccccccccccccccccccc
get a
get b
get c

docker exec -it redis7000 redis-cli -c -p 7000
keys *

docker exec -it redis7000 redis-cli -c -p 7001
keys *

docker exec -it redis7000 redis-cli -c -p 7002
keys *

CLUSTER KEYSLOT a
CLUSTER KEYSLOT b
CLUSTER KEYSLOT c


CLUSTER KEYSLOT {user}a
CLUSTER KEYSLOT {user}b
CLUSTER KEYSLOT {user}c
CLUSTER KEYSLOT {user}

mset x xxxxxx y yyyy z zzzzzzzz

mset {user}x xxxxxx {user}y yyyy {user}z zzzzzzzz

mget x y z

mget {user}x {user}y {user}z

主从切换、故障转移:

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

docker stop redis7000

docker logs redis7004
docker exec -it redis7004 redis-cli -c -p 7004
info replication

docker stop redis7004

docker exec -it redis7001 redis-cli -c -p 7001
set d dddddddddddd

docker start redis7004

docker logs redis7004

docker start redis7000

docker logs redis7000

docker exec -it redis7001 redis-cli -c -p 7001
set d dddddddddddd

Jedis客户端API:

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

        Set<HostAndPort> set = new HashSet<>();
        set.add(new HostAndPort("192.168.64.140", 7000));
        set.add(new HostAndPort("192.168.64.140", 7001));
        set.add(new HostAndPort("192.168.64.140", 7002));
        set.add(new HostAndPort("192.168.64.140", 7003));
        set.add(new HostAndPort("192.168.64.140", 7004));
        set.add(new HostAndPort("192.168.64.140", 7005));

        JedisCluster j = new JedisCluster(set);
        for (int i = 0; i < 100; i++) {
            j.set("k" + i, "v" + i);
        }
        j.close();

Key操作命令

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

docker rm -f $(docker ps -aq) --volumes

redis --help

redis -l

redis -lx

redis -c

docker exec -it redis7000 redis-cli -c -p 7000

set a aaaaaaaaaa
set b bbbbbbbbbbbbb
set c ccccccccccccc
set d dddddddddd


keys *
	查询所有Key
exists a
	Key是否存在
type a
	对应的值的数据类型
del d
	删除Key,等待从所有主从服务器都删除完成
unlink c
	异步删除,主机删除后直接返回,不等待从机删除
expire a 10
	设置过期时间(秒)
ttl a
	查看过期时间

库操作

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

config get databases
	查看数据库数量
select 1
	切换库,不支持集群模式
dbsize
	当前库中键的数量
flushdb
	清空当前库
flushall
	清空所有库

Redis 5种基本数据类型

  • String

  • List

  • Set

  • Hash

  • Zset

String

  • string可以做数字运算

  • string单个字符串最大存储512M(2^32 - 1)

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

docker exec -it redis7000 redis-cli -c -p 7000

set key value
--------------------------
set a aaaa

get key
--------------------------
get a

append key value
	在末尾追加字符串
--------------------------
append a xxxxx

strlen key
	字符串长度
--------------------------
strlen a

setnx key value
	键不存在才设置, nx -- not exsits
--------------------------
setnx c cccccccccccccccc
setnx c xxxxxxxxxxx

set key value nx
	nx -- not exsits
--------------------------
set c yyyyyyyyyyyyyy nx


incr key
	递增,原子操作
-------------------------
set d 1
incr d
incr d
incr d


decr key
	递减,原子操作
-------------------------
decr d
decr d
decr d
decr d


incrby key step
------------------------
incrby d 10000

decrby key step
------------------------
decrby d 3000

mset key value key value ......
----------------------------------
mset a aaaa b bbbbb c cccccc d ddddd e eeeee f fffff

mset {x}a aaaa {x}b bbbbb {x}c cccccc {x}d ddddd {x}e eeeee {x}f fffff


mget key key ......
----------------------------------
mget a b c d

mget {x}a {x}b {x}c {x}d


msetnx key value key value ......
	所有的键都不存在才设置,有任意一个键存在全部都失败
----------------------------------
msetnx {x}g ggggg {x}h hhhhh {x}i iiiii

msetnx {x}i iiiii {x}j jjjjj {x}k kkkkk

getrange key start end
	截取字符串,包含start和end
------------------------------------
set a abcdefghijklmn

getrange a 0 3
getrange a 9 -1  # 从下标9开始,到末尾最后一个字符

setrange key start value
	替换子串,从start位置开始进行替换
------------------------------------
setrange a 5 ---- 
get a
	
expire key seconds
	设置键的过期时间
-------------------------
set x xxxxxxxxxxxx
expire x 10
	
ttl key
	查看过期时间
-------------------------
ttl x

setex key seconds value
	添加数据,同时指定过期时间
	set + expire
-------------------------
setex x 10 xxxxxxxxxxxx
ttl x

set key value EX|PX 时间
	EX以秒为单位设置过期时间
	PX以毫秒为单位设置过期时间
-------------------------
set c cccccccc EX 10
ttl c

set c cccccccc ex 10 nx


getset key value
	取旧值,换新值
-------------------------
set a aaaaaaaaaaaa

getset a xxxxxxxxxxxxxx

get a

List

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

# 清空数据
get a
flushall
get b
flushall
get c 
flushall

lpush/rpush key value value ......
	左侧添加/右侧添加
-------------------------------------
lpush a 1 2 3

rpush a x y z

lrange key start end
	从左侧按范围取数据
	-1表示末尾
-----------------------------------
lrange a 0 -1

lpop/rpop  key  [弹出的数量]
	从左侧或右侧弹出值
-----------------------------------
lpop a
rpop a
rpop a 2
	
	
rpoplpush from_key to_key
	右侧弹出,再加入左侧
-----------------------------------
rpush {x}a 1 2 3 4 5 6 7 8 9

lrange {x}a 0 -1

rpoplpush {x}a {x}b
rpoplpush {x}a {x}b
rpoplpush {x}a {x}b
lrange {x}a 0 -1
lrange {x}b 0 -1

rpoplpush {x}a {x}a
rpoplpush {x}a {x}a
rpoplpush {x}a {x}a


lindex key index
	按下标取数据
------------------------------
lindex {x}a 0
lindex {x}a 1
lindex {x}a 2
lindex {x}a 3


llen
	列表长度
------------------------------
llen {x}a
	
linsert key before/after value new_value
	在指定的的值的前后添加数据
------------------------------
linsert {x}a before 5 x
lrange {x}a 0 -1


lrem key n value
	从左侧找到n个给定的值删掉,n是0表示删除全部指定的值
------------------------------
lrem {x}a 2 6
	
lset key  index new_value
	替换index位置
------------------------------
lset {x}a 3 /
lrange {x}a 0 -1

Set

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

# 清空数据
get a
flushall
get b
flushall
get c 
flushall

sadd key value value .....
-------------------------------
sadd a 11 22 33 44 55 11 22 33 66 77

smembers key
	取所有值
-------------------------------
smembers a
	
sismember key value
	值是否在Set中存在
-------------------------------
sismember a 33
	
scard key
	元素数量
-------------------------------
scard a
	
srem key value value ......
	删除值
-------------------------------
srem a 33 44
smembers a
	
spop key [count]
	随机弹出
-------------------------------
spop a
spop a 4

	
srandmember key n
	随机访问n个值(不会删除)
-------------------------------
srandmember a 2



smove from to value
	把值从一个集合移动到另一个集合
-------------------------------
sadd {x}a 11 22 33 44 55 66

smove {x}a {x}b 55

smembers {x}a
smembers {x}b


sinter key key ......
	多个Set集合的交集
sunion key key ......
	并集
sdiff key key ......
	差集
-------------------------------
sadd {x}s1 11 22 33 44
sadd {x}s2 22 44 66 88

sinter {x}s1 {x}s2
sunion {x}s1 {x}s2
sdiff {x}s1 {x}s2

Hash

  • 多数情况下直接使用redis存放兼职数据

  • 如果多个属性的对象中,有的属性要频繁修改,可以考虑使用Hash

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

# 清空数据
get a
flushall
get b
flushall
get c 
flushall

hset key field value field value ......
--------------------------------------------
hset a k1 v1 k2 v2 k3 v3

hget key field
--------------------------------------------
hget a k1
hget a k2
hget a k3


hexists key field
--------------------------------------------
hexists a k1
hexists a k9


hkyes key
	列出所有field
--------------------------------------------
hkeys a
	
hvals key
	列出所有value
--------------------------------------------
hvals a
	
hincrby key field increment
	在数字上加一个值
--------------------------------------------
hset a k4 1

hincrby a k4 10
hget a k4
	
hsetnx key field value
	不存在才添加
--------------------------------------------
hsetnx a k1 xxxxxx

Zset

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

# 清空数据
get a
flushall
get b
flushall
get c 
flushall

zadd key score value score value ......
---------------------------------------------
zadd a 23234 aaaa 5645 bbbb 23125345 cccc 3 ddd 6456 eee

zadd a 8 ddd

zrange key start end
	取数据,end用-1表示末尾
---------------------------------------------
zrange a 0 -1
	
zrange key start end withscores
	取数据时同时返回得分
---------------------------------------------
zrange a 0 -1 withscores


zrevrange key start end [withscores]
	按得分从高到低排序
---------------------------------------------
zrevrange a 0 -1 withscores
zrevrange a 0 2 withscores

	
	
zrangebyscore key min max [withscores] [limit start count]
	按分值范围取数据
---------------------------------------------
zrangebyscore a 1000 10000 withscores

zrangebyscore a 0 100000000 withscores limit 0 3
zrangebyscore a 0 100000000 withscores limit 3 3



zrevrangebyscore key min max [withscores] [limit start count]
	按得分从高到低排序
--------------------------------------------
zrevrangebyscore a 10000 1000 withscores

zrevrangebyscore a 100000000 0 withscores limit 0 3
zrevrangebyscore a 100000000 0 withscores limit 3 3
	

zincrby key increment member
	 增加积分
----------------------------------------
zincrby a 1 ddd

zincrby a -1 ddd

zrem key member member ......
	删除
----------------------------------------
zrem a cccc aaaa

zcount key min max
	统计分数区间内的数据量
----------------------------------------
zcount a 0 1000

zrank key member
	该数据的排名(从小到大)
----------------------------------------
zrank a eee

zrevrank key member
	该数据的排名(从大到小)
----------------------------------------
zrevrank a eee

Bitmaps

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

setbit key offset value
	只能设置0和1
--------------------------------------
setbit active-users-20220720 7457634 1
setbit active-users-20220720 4356462 1
setbit active-users-20220720 6753454 1
setbit active-users-20220720 2353464 1
setbit active-users-20220720 43526 1
setbit active-users-20220720 567345623 1
setbit active-users-20220720 452364 1
setbit active-users-20220720 1236743 1
setbit active-users-20220720 45463526 1
setbit active-users-20220720 57545 1
setbit active-users-20220720 67457432 1
setbit active-users-20220720 4280000000 1



bitcount key [start end]
	值是1的数据量
	start, end 指的是哪个字节,不是位
--------------------------------------------------
bitcount active-users-20220720 0 535000000
bitcount active-users-20220720 0 -1



getbit key offset
--------------------------------------------------
getbit active-users-20220720 1236743
getbit active-users-20220720 4356462
getbit active-users-20220720 57545


bitop operation destnation_key key key ......
	对多个 bitmap 进行运算,运算结果保存到目标对象
	operation
		and
		or
		not
		xor

HypperLogLog

基数统计:使用更小的内存,对大量数据进行统计。

使用一种特殊的统计学算法完成数据量的统计,存在误差

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

# 清理数据
flushall

pfadd key value value ......
--------------------------------------
pfadd {x}a aaaaaa bbbbbbbb cccc ddddd aaaaaa bbbbbbbb cccc ddddd aaaaaa aaaaaa cccc cccc cccc

pfadd {x}b xxx yyy zz zz xxx zz cccc



pfcount key key key ......
--------------------------------------
pfcount {x}a
pfcount {x}b
pfcount {x}a {x}b




pfmerge destnation_key key key ......
--------------------------------------
pfmerge {x}x {x}a {x}b

pfcount {x}x

Geospatial

保存地理信息(经纬度)

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

geoadd key 经度 纬度 地名 经度 纬度 地名 ......
	geoadd city 121.47 31.23 shanghai 106.5 29.53 chongqing 114.05 22.52 shenzhen 116.38 39.9 beijing
---------------------------------------------------

geopos key 地名 地名 ......
	获取坐标位置
-------------------------------------
geopos city shenzhen chongqing
	

geodist key 地名 地名 [m|km|ft|mi]
	两地的距离 distance
-------------------------------------
geodist city shenzhen chongqing

georadius key 经度 维度 距离 [m|km|ft|mi]
	该地点周边范围内的地地点
-------------------------------------
georadius city 110 25 500 km
georadius city 110 25 700 km
georadius city 110 25 2000 km

Pub-Sub

Publish - Subscribe

发布和订阅模式

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

# 消费者
docker exec -it redis7000 redis-cli -c -p 7000

subscribe myChannel

# 生产者
docker exec -it redis7000 redis-cli -c -p 7000

publish myChannel 11111111111
publish myChannel 222222222
publish myChannel 333333333333333

Redis事务

  • 功能弱

  • Multi 组队命令序列 类似 mysql 的 begin multi 后可以执行多步数据操作

  • Exec 执行multi的命令序列 相当于 mysql 的 commit

  • Discard 放弃执行命令序列 相当于回滚

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

get {x}

multi
set {x}k1 aaaaaaaaaaaaa
set {x}k2 bbbbbbbbbbbbb
set {x}k3 ccccccccccccccccc
del {x}k1

exec

multi
set {x}k4 ccccccccccccc
set {x}k5 gggggggggggggg

discard

Redis持久化

redis内存的数据,持久化存储到磁盘,当redis重启时,可以从持久化的数据快速回复内存数据。

RDB

Redis Database

内存数据镜像,把内存所有数据拍一个快照,把数据全部保存到一个磁盘文件:dump.rdb

默认在达到一定条件时才会自动保存,或者可以手动调用:

  • save 同步保存

  • bgsave 异步保存

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

redis -cx

redis -1

docker exec -it redis bash
# 查看文件,没有持久化文件dump.rdb
ls

# 在 redis中添加3条数据
redis-cli
set a aaaaaaaaaa
set b bbbbbbbbb
set c cccccccccccccc

# 退出redis客户端
exit

# 查看文件,仍然没有持久化文件dump.rdb
ls

# 进入redis客户端,执行持久化操作,把内存数据持久化存储到磁盘
redis-cli

save
bgsave

# 退出客户端,查看文件
exit
ls

# 退出容器
exit

# 重启redis容器,redis重启时,会从持久化文件dump.rdb恢复数据
docker restart redis

# 查看数据
docker exec -it redis redis-cli
keys *

# 添加新的数据
set d ddddddd
set e eeeeeeeeeee
# 退出redis-cli客户端
exit

# 退出redis容器
exit

# 杀掉容器进程
docker kill redis

# 重启redis容器,redis重启时,会从持久化文件dump.rdb恢复数据
docker restart redis

# 查看数据,只有旧数据,新数据会丢失
docker exec -it redis redis-cli
keys *

可以设置redis自动保存快照,redis.conf中添加配置,例如:

  • save 3600 1:1小时内,有一条数据改动

  • save 300 100:5分钟内,有100条数据改动

  • save 60 10000:1分钟,有10000条数据改动

RDB的优缺点:

  • 优点: 恢复数据速度快

  • 缺点:

    • 持久化时存储数据效率低

    • 不能实时存储

    • 可能丢失少量新数据

写时复制:

  • 保存快照时,先写到临时文件,完成后删除dump.rdb,把临时文件盖明成 dump.rdb

  • 临时文件写入失败数据损坏,也不会影响dump.rdb

dump.rdb文件修复:

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

docker exec -it redis bash

redis-check-rdb dump.rdb

AOF

AOF - Append Only File

记录数据操作指令,每执行一条增删改的命令,都会实时地追加到 aof 日志文件。

默认不开启AOF,需要在配置文件中开启AOF:

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

appendonly yes		#默认是no

AOF的优缺点:

  • 优点

    • 持久化效率高

    • 实时持久化

    • 不会丢失数据

  • 缺点

    • 恢复数据时效率低

    • aof中记录的所有命令进行重放,效率低

修复AOF文件:

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

redis-check-aof --fix appendonly.aof

rewrite压缩:

对文件中记录的指令,能合并的指令合并成一条执行。

  • 文件大小达到min-size时触发,每增长100%时再次触发

    --javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

    auto-aof-rewrite-percentage 100
    auto-aof-rewrite-min-size 64mb

aof测试

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

0. 删除redis
docker rm -f redis --volumes

1. 上传配置文件redis.conf,其中打开了aof

2. 启动新的redis容器,挂载这个配置文件
docker run -d --name redis \
-v ~/redis.conf:/redis.conf \
redis \
redis-server /redis.conf

3. 在redis中添加数据
docker exec -it redis bash

redis-cli
set a aaaaaaaaaa
set b bbbbbbbbbbbb
set c ccccccc
exit


4. 查看aof文件
ls
cat appendonly.aof
exit

如果是新版本redis执行:
cd appendonlydir/
ls
cat appendonly.aof.1.incr.aof
exit

5. 测试数据恢复
docker restart redis
docker exec -it redis redis-cli
keys *

官方建议使用 RDB+AOF 混合模式

存储:

  • RDB完整存储内存数据

  • RDB之后的增量数据,使用AOF来记录增量数据

恢复:

  • 从RDB恢复绝大多数数据

  • 重放AOF,恢复少量增量数据

默认已经开启了混合模式:

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

aof-use-rdb-preamble yes

需要手动打开AOF:

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

appendonly yes		#默认是no

高可用

主从

  • 主从复制

  • 读写分离

  • 从服务器重启 可以自动从主服务器同步数据

  • 主服务器宕机 从服务器会等待主服务器重启

  • 主从复制原理 从服务器上线 全量复制 从服务器发送请求,主服务器rdb,将rdb文件发送给从服务器导入数据 主服务器更改数据时,自动向从服务器同步数据 增量复制

  • 主-从-从结构 从服务器的从服务器 减轻主服务器数据同步的压力

  • 命令 slaveof ip port 作为制定服务器的从服务器 slaveof no one 从服务器转为主服务器

哨兵

  • 监控主从集群,自动主从切换

  • 选举依据 1.优先级 replica-priority 2. 偏移量 获取主机的数据数量 3. runid 随机的40位id值

Redis Cluster 集群

  • 去中心化

  • 配置 cluster-enabled yes cluster-config-file node.conf cluster-node-timeout 15000 #失联时间

  • 集群规划原则 主服务不在相同ip 从服务不在他的主服务ip

  • 哈希槽 计算哈希槽值 cluster keyslot key cluster keyslot key{group} 哈希槽中的数据量 cluster countkeysinslot slot 只能查看当前服务器哈希槽 获得哈希槽中存储的键 cluster getkeysinslot slot count count指定获取的数量

    --javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

    cluster keyslot 5345
    cluster keyslot 8542
    cluster keyslot 2573
    
    cluster keyslot 5345{news}
    cluster keyslot 8542{news}
    cluster keyslot 2573{news}
    
  • 数据操作 set key value 使用key计算哈希槽 set key{group} value 使用group计算哈希值 mset key{group} value key{group} value ... 添加多个数据必须指定组,且是相同的组 get key get key{group} 添加时有组名,获取数据时也必须有组名

  • 故障恢复 主从切换 一台主机宕机,它的从服务器自动成为主机 之前的主机重启后成为从机 一组主从全部宕机 哈希槽覆盖不全,整个集群不可用 cluster-require-full-coverage yes 默认 哈希槽覆盖不全,其他服务器仍提供服务 cluster-require-full-coverage no

  • api JedisCluster j = new JedisCluster(set); j.set("k", "kkk");

  • 缺点 不支持多键操作 不支持lua脚本 旧的集群向cluster迁移不方便

击穿、穿透和雪崩

击穿

image-20221213201439032

  • 热门数据,突然过期,访问被打倒数据库。

  • 击穿Redis,穿到数据库。

​ 数据库访问短时激增 ​ redis正常运行

  • 可能的场景 突发热门访问 一个key正被大量访问 这个key过期

  • 解决方案 预先设置热门数据 实时调整过期时间,自动续期 使用锁、限流 缓存数据不存在,先把数据存入redis

穿透

image-20221213203001867

  • Redis中没有数据,数据库中也没有数据

  • 大量执行这种访问,会拖慢数据库

​ 访问量增大 ​ redis数据命中率低 ​ 数据库也没有数据 ​ 可能的场景

  • 访问的数据在redis中数据不存在

  • 大量访问非正常数据,数据在数据库中也不存在 非正常访问 黑客攻击

  • 解决方案

    • 对不存在的数据,缓存空值

    • 白名单 可访问的数据id作为偏移量存入bitmaps 访问时检查bitmaps

    • 进行实时监控 监控攻击

雪崩

大量key集中过期,数据库访问短时激增

  • 解决方案

    • 多级缓存架构

      • nginx缓存

      • ehcache | guava

      • redis

    • 自动续期、自动更新数据 即将过期的数据提前更新数据

    • 对过期时间使用随机值 分散过期时间

    • 锁 | 队列

** 分布式锁

方案1-setnx+expire

setnx - Set if Not Exists,如果锁不存在,才能添加成功

  • setnx key value 加锁 没有,可以放入 数据存在,无法放入

  • del key 解锁

  • 死锁 长时间不释放锁或忘记释放锁,会发生死锁 设置过期 expire key seconds

方案2-SET的扩展命令(SET key value NX EX|PX time)

合并两个命令,数据不存在时添加数据并设置过期时间:

  • setnx

  • expire

加锁原子性 避免两步操作中间出现故障,可以一步完成 set key value nx ex 15 #15秒 set key value nx px 15000 #15000毫秒

方案3-SETNX + value值是系统时间+过期时间

防止误删其他客户端的锁

set my-lock 系统当前时间+过期时间

删除锁时:

  1. 1.

    获取my-lock的值,进行比对,是不是自己设置的值

  2. 2.

    如果是,才能执行删除

方案4-SET EX PX NX + 校验唯一随机值,再释放锁

例如uuid,防止误删其他客户端的锁

** 方案5-使用Lua脚本(包含get + del两条指令)

​ 防止误删时的多步操作问题: ​ a比对值一致 ​ a正准备删除锁时,锁过期 ​ b加锁 ​ a执行删锁,这时会误删b的锁 ​ lua脚本原子操作 ​ 解锁原子性 ​ 比对+删锁

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

if (redis.call('get', KEYS[1]) == ARGV[1]) 
	then return redis.call('del', KEYS[1]);
else return 0;
end; 

# 调用Lua脚本:
# 参数说明: 键值对数量 key value key value...
eval "脚本" 1 mylock ijfu0983u48t

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

# 加锁
set mylock 123 nx ex 30


# 解锁
eval "if (redis.call('get', KEYS[1]) == ARGV[1]) then return redis.call('del', KEYS[1]);	else return 0; end;" 1 mylock 456

加锁:

set myLock u5y43t33 nx ex 60

解锁:

执行Lua脚本解锁

Jedis API:

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

public class Test1 {
    static Jedis j;

    public static void main(String[] args) {
        j = new Jedis("192.168.64.140", 6379);

        String lock = UUID.randomUUID().toString();
        if (lock("myLock", lock)) {
            System.out.println("已加锁");
            System.out.println("执行业务");


            unlock("myLock", lock);
            System.out.println("已解锁");
        } else {
            System.out.println("加锁失败");
        } 

    }

    static boolean lock(String lockKey, String lockVal) {
        String result = j.set(lockKey, lockVal, "nx", "ex", 60);
        return "OK".equals(result);
    }

    static void unlock(String lockKey, String lockVal) {

        /*
        if (redis.call('get', KEYS[1]) == ARGV[1])
            then return redis.call('del', KEYS[1]);
            else return 0;
        end;
         */

        String script =
                "if (redis.call('get', KEYS[1]) == ARGV[1])\n" +
                "    then return redis.call('del', KEYS[1]);\n" +
                "    else return 0;\n" +
                "end;";

        j.eval(script, 1, lockKey, lockVal);
    }

方案6-多机实现的分布式锁Redlock

主从切换加锁失败:

主服务器加的锁,没有成功向从机复制的情况下,主机宕机。从机上没有锁。

Redlock:

对多台无关的服务器遍历加锁、解锁,只要多数成功就算成功

** 方案7-开源框架~Redisson

Redisson是一个类似于Jedis的Redis客户端API,提供了分布式锁的API:

  • 可重入锁

  • 公平锁

  • 联锁

  • 红锁

  • 读写锁