双写一致性
双写模式
对性能要求比较高的业务,可以采用异步双写模式。
对数据要求比较高的业务,可以采用同步双写模式。
更新策略
凌晨停机,使用单线程更新:
1、适用场景少,例如微信必须二十四小时提供服务。
先更新 Redis,再更新 MySQL:
1、在业务层面,应该先更新 MySQL。
2、并发环境下,A、B 更新 Redis,B、A 更新 MySQL,导致数据不一致。
先更新 MySQL,再更新 Redis:
1、并发环境下,A、B 更新 MySQL,B、A 更新 Redis,导致数据不一致。
先删除 Redis,再更新 MySQL:
1、在业务层面,应该先更新 MySQL。
2、并发环境下,A 删除 Redis 成功,B 访问 MySQL 旧值,A 更新 MySQL 成功,B 回写 Redis 旧值,导致数据不一致,后续访问均是旧值。
解决方法:延时双删(并发环境下,A 删除 Redis 成功,B 访问 MySQL 旧值,A 更新 MySQL 成功,B 回写 Redis 旧值。A 延时一段时间,删除 Redis 脏数据。)
public void updateData() {
deleteRedisData();
updateMySQLData();
delayedSleep();
deleteRedisData();
}
为什么延时?
为了等待 B 访问 MySQL 旧值,B 回写 Redis 旧值,最后删除这次脏数据。
但是延迟的时间难以确定,所以并不推荐使用这种方法。
为什么双删?
第一次删除:避免访问 Redis 旧值。
第二次删除:避免访问 Redis 脏数据。
先更新 MySQL,再删除 Redis:(推荐)
1、并发环境下,A 更新 MySQL 还未删除 Redis,B 获取 Redis 旧值。
解决方法:最终一致性允许旧值短暂存在,重新执行业务逻辑获取新值。
2、更新 MySQL 成功,删除 Redis 失败,导致数据不一致。
解决方法:RabbitMQ 重试。
AlibabaCanal
https://github.com/alibaba/canal
https://github.com/alibaba/canal/releases/download/canal-1.1.6/canal.deployer-1.1.6.tar.gz
https://github.com/alibaba/canal/wiki/ClientExample
https://blog.csdn.net/cowbin2012/article/details/89335693
https://www.zhihu.com/question/486992560/answer/2851230529
原理:伪装 MySQL Slave 向 MySQL Master 发送 dump 协议,订阅 binlog 日志。
# 修改 my.cnf 文件,开启 binlog 日志
[mysqld]
log-bin=binlog
server-id=1
binlog-format=row
# 执行 MySQL 相关命令
DROP USER IF EXISTS 'canal' @'%';
CREATE USER 'canal' @'%' IDENTIFIED BY 'canal';
GRANT ALL PRIVILEGES ON *.* TO 'canal' @'%' IDENTIFIED BY 'canal';
FLUSH PRIVILEGES;
SHOW VARIABLES LIKE '%log_bin%';
SELECT * FROM mysql.`user`;
# 下载安装 Canal
cd /docker/canal/
tar -zxvf /docker/canal/canal.deployer-1.1.6.tar.gz
rm -rf canal.deployer-1.1.6.tar.gz
vim ./conf/example/instance.properties
canal.instance.master.address=127.0.0.1:3306
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal
canal.mq.topic=example
vim ./conf/canal.properties
canal.port = 11111
./bin/startup.sh
# 测试数据
CREATE DATABASE `db_canal` CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_general_ci' ;
USE `db_canal` ;
BEGIN ;
CREATE TABLE `tb_user` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '编号',
`name` VARCHAR (255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '名字',
`description` VARCHAR (512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '描述',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`is_deleted` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '是否删除(1 表示是,0 表示否)',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = INNODB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户' ;
COMMIT ;
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.1.0</version>
</dependency>
import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.client.CanalConnectors;
import com.alibaba.otter.canal.protocol.CanalEntry.*;
import com.alibaba.otter.canal.protocol.Message;
import com.google.protobuf.InvalidProtocolBufferException;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class SimpleCanalClientExample {
public static void deleteRedisData() {
}
public static void main(String[] args) throws InterruptedException, InvalidProtocolBufferException {
CanalConnector canalConnector = CanalConnectors.newSingleConnector(
new InetSocketAddress("127.0.0.1", 11111), "example", "", ""
);
try {
canalConnector.connect();
canalConnector.subscribe("db_canal.tb_user");
canalConnector.rollback();
int emptyCount = 0;
while (emptyCount < 2 * 60) {
Message message = canalConnector.getWithoutAck(1000);
long id = message.getId();
int size = message.getEntries().size();
if (id == -1 || size == 0) {
emptyCount++;
System.out.println("emptyCount:" + emptyCount);
TimeUnit.SECONDS.sleep(1);
} else {
emptyCount = 0;
printEntryAndUpdateRedis(message.getEntries());
deleteRedisData();
}
canalConnector.ack(id);
}
} finally {
canalConnector.disconnect();
}
}
private static void printEntryAndUpdateRedis(List<Entry> entryList) throws InvalidProtocolBufferException {
for (Entry entry : entryList) {
if (entry.getEntryType() == EntryType.TRANSACTIONBEGIN || entry.getEntryType() == EntryType.TRANSACTIONEND) {
continue;
}
RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
EventType eventType = rowChange.getEventType();
System.out.println();
System.out.println("=====" +
"LogfileName=" + entry.getHeader().getLogfileName() +
"LogfileOffset:" + entry.getHeader().getLogfileOffset() +
"SchemaName" + entry.getHeader().getSchemaName() +
"TableName()" + entry.getHeader().getTableName() +
"EventType" + eventType
);
System.out.println();
for (RowData rowData : rowChange.getRowDatasList()) {
if (eventType == EventType.INSERT) {
System.out.println();
printColumn(rowData.getAfterColumnsList());
System.out.println();
} else if (eventType == EventType.DELETE) {
System.out.println();
printColumn(rowData.getBeforeColumnsList());
System.out.println();
} else if (eventType == EventType.UPDATE) {
System.out.println();
System.out.println("===== UPDATE before");
printColumn(rowData.getBeforeColumnsList());
System.out.println("===== UPDATE after");
printColumn(rowData.getAfterColumnsList());
System.out.println();
}
}
}
}
private static void printColumn(List<Column> columnList) {
for (Column column : columnList) {
System.out.println(column.getName() + ":" + column.getValue());
}
}
}
双检加锁
如果查询 Redis 没有数据,则加锁,再次查询 Redis 获取数据。
如果查询 Redis 仍然没有数据,则查询 MySQL 获取数据,最后更新 Redis 回写数据。
public String getData() {
String data = getRedisData();
if (data == null || "null".equals(data)) {
synchronized (this) {
data = getRedisData();
if (data == null || "null".equals(data)) {
data = getMySQLData();
updateRedisData(data);
}
}
}
return data;
}
布隆过滤器
作用
用于判断一个元素是否存在集合中,能够减少内存占用,但是存在哈希冲突。
因为存在哈希冲突,所以只能添加元素,不能删除元素,避免增加误判率。
原理
底层通过一个位数组和多个哈希函数实现。
判断结果:“存在” 不一定存在,“不存在” 一定不存在。

GoogleGuava
BloomFilter -> Redis -> MySQL
https://github.com/google/guava/blob/master/guava/src/com/google/common/hash/BloomFilter.java
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import java.nio.charset.StandardCharsets;
public class BloomFilterDemo {
public static void main(String[] args) {
BloomFilter<CharSequence> bloomFilter = BloomFilter.create(
Funnels.stringFunnel(StandardCharsets.UTF_8),
5000,
0.03
);
bloomFilter.put("abc");
System.out.println(bloomFilter.mightContain("abc"));
System.out.println(bloomFilter.mightContain("ABC"));
}
}
预热雪崩穿透击穿
预热
系统上线时 Redis 还没有数据,此时这些请求都会直接访问 MySQL,如果高并发很有可能直接宕机。
@PostConstruct 可以修饰一个非 static 的 void 方法,用于完成初始化配置。
执行顺序:构造函数 -> @Resource -> @PostConstruct
@PostConstruct
public void init() {
insertRedisData();
}
雪崩
多个 key 同时过期(抑或 Redis 故障),此时这些请求都会直接访问 MySQL,如果高并发很有可能直接宕机。
解决方法:
1、多级缓存:Redis + Ehcache
2、错开过期:在过期时间的基础上添加几分钟的随机值, 降低过期时间的重复率。
3、主备策略:主 key 设置过期,备 key 永不过期,当主 key 过期时返回备 key 数据。
4、熔断限流
5、集群
穿透
key 在 Redis 和 MySQL 均不存在,此时这些请求都会直接访问 MySQL,如果高并发很有可能直接宕机。
解决方法:
1、布隆过滤器
2、缓存空值
击穿
某个 key 过期,此时这些请求都会直接访问 MySQL,如果高并发很有可能直接宕机。
解决方法:
1、永不过期
2、双检加锁
缓存雪崩针对多个 key 而言, 缓存击穿针对某个 key 而言。
BigKey
测试数据
docker exec -it redis6379 /bin/bash
for((i=1;i<=100*10000;i++)); do echo "set k$i v$i" >> /tmp/redisString.txt; done;
cat /tmp/redisString.txt | redis-cli -h 127.0.0.1 -p 6379 -a redis --pipe
for((i=1;i<=100*10000;i++)); do echo "hmset myHash k$i v$i" >> /tmp/redisHash.txt; done;
cat /tmp/redisHash.txt | redis-cli -h 127.0.0.1 -p 6379 -a redis --pipe
如何禁用危险命令?
# 修改 redis.conf 文件
rename-command config ""
rename-command flushall ""
rename-command flushdb ""
rename-command keys ""
rename-command save ""
海量数据如何遍历?
因为 Redis 命令是原子性的,keys 命令会一次性返回所有数据从而导致 Redis 阻塞,所以生产环境禁止使用。
而 scan 命令是一个基于游标的迭代器,它会随机选择一个起始位置,然后从这个位置开始遍历,返回部分数据。
这个命令会返回一个游标值,表示下一次迭代的起始位置,用户需要使用这个新的游标值来继续迭代过程,使用 0 开始新的迭代,返回 0 表示结束迭代。
使用 scan 命令可能获取重复的数据,后端需要进行去重。
scan 命令用于 string 类型,hscan 命令用于 hash 类型,sscan 命令用于 set 类型,zscan 命令用于 zset 类型。
多大属于 BigKey?
【阿里巴巴 Redis 开发规范】
string 大小不要超过 10KB,即 10000bytes
list、hash、set、zset 元素不要超过 5000 个
BigKey 的危害?
内存分配不均、网络带宽过大、过期删除阻塞。
如何发现 BigKey?
通过 --bigkeys 查看每种数据类型最大的 BigKey,以及键值个数和平均大小。
# redis-cli -h 127.0.0.1 -p 6379 -a redis --bigkeys
-------- summary -------
Sampled 1000001 keys in the keyspace!
Total key length in bytes is 6888902 (avg len 6.89)
Biggest hash found '"myHash"' has 1000000 fields
Biggest string found '"k1000000"' has 8 bytes
0 lists with 0 items (00.00% of keys, avg size 0.00)
1 hashs with 1000000 fields (00.00% of keys, avg size 1000000.00)
1000000 strings with 6888896 bytes (100.00% of keys, avg size 6.89)
0 streams with 0 entries (00.00% of keys, avg size 0.00)
0 sets with 0 members (00.00% of keys, avg size 0.00)
0 zsets with 0 members (00.00% of keys, avg size 0.00)
通过 memory usage 查看某个键值的内存占用大小,单位 bytes。
# memory usage k1000000
(integer) 60
Redis 中每个 string 类型的 key 会占用一些额外的内存空间,所以实际上 key 的内存占用大小会比字符串值的字节数要大。
如何删除 BigKey?
string 使用 unlink 删除。
public void delBigStringKey(String host, int port, String password, String bigStringKey) {
try (Jedis jedis = new Jedis(host, port)) {
if (password != null && !"".equals(password)) {
jedis.auth(password);
}
jedis.unlink(bigStringKey);
}
}
list 使用 ltrim 删除。
public void delBigListKey(String host, int port, String password, String bigListKey) {
try (Jedis jedis = new Jedis(host, port)) {
if (password != null && !"".equals(password)) {
jedis.auth(password);
}
int counter = 0;
int leftIndex = 100;
Long listLength = jedis.llen(bigListKey);
while (counter < listLength) {
jedis.ltrim(bigListKey, leftIndex, listLength);
counter += leftIndex;
}
jedis.del(bigListKey);
}
}
hash 使用 hscan + hdel 删除。
public void delBigHashKey(String host, int port, String password, String bigHashKey) {
try (Jedis jedis = new Jedis(host, port)) {
if (password != null && !"".equals(password)) {
jedis.auth(password);
}
ScanParams scanParams = new ScanParams().count(100);
String cursor = "0";
do {
ScanResult<Map.Entry<String, String>> scanResult = jedis.hscan(bigHashKey, cursor, scanParams);
List<Map.Entry<String, String>> entryList = scanResult.getResult();
if (entryList != null && !entryList.isEmpty()) {
for (Map.Entry<String, String> entry : entryList) {
jedis.hdel(bigHashKey, entry.getKey());
}
}
cursor = scanResult.getCursor();
} while (!"0".equals(cursor));
jedis.del(bigHashKey);
}
}
set 使用 sscan + srem 删除。
public void delBigSetKey(String host, int port, String password, String bigSetKey) {
try (Jedis jedis = new Jedis(host, port)) {
if (password != null && !"".equals(password)) {
jedis.auth(password);
}
ScanParams scanParams = new ScanParams().count(100);
String cursor = "0";
do {
ScanResult<String> scanResult = jedis.sscan(bigSetKey, cursor, scanParams);
List<String> memberList = scanResult.getResult();
if (memberList != null && !memberList.isEmpty()) {
for (String member : memberList) {
jedis.srem(bigSetKey, member);
}
}
cursor = scanResult.getCursor();
} while (!"0".equals(cursor));
jedis.del(bigSetKey);
}
}
zset 使用 zscan + zrem 删除。
public void delBigZSetKey(String host, int port, String password, String bigZSetKey) {
try (Jedis jedis = new Jedis(host, port)) {
if (password != null && !"".equals(password)) {
jedis.auth(password);
}
ScanParams scanParams = new ScanParams().count(100);
String cursor = "0";
do {
ScanResult<Tuple> scanResult = jedis.zscan(bigZSetKey, cursor, scanParams);
List<Tuple> tupleList = scanResult.getResult();
if (tupleList != null && !tupleList.isEmpty()) {
for (Tuple tuple : tupleList) {
jedis.zrem(bigZSetKey, tuple.getElement());
}
}
cursor = scanResult.getCursor();
} while (!"0".equals(cursor));
jedis.del(bigZSetKey);
}
}
惰性删除
开启 lazyfree 后,主动或被动删除 BigKey 不会立即释放内存,而是将其标记后放入队列,由 BIO 线程处理,避免主线程阻塞。
# 修改 redis.conf 文件,使用惰性删除
lazyfree-lazy-eviction no
lazyfree-lazy-expire no
lazyfree-lazy-server-del yes
replica-lazy-flush yes
lazyfree-lazy-user-del yes
长文本
Redis 对于长文本的支持并不好,应该使用文档型数据库,比如 MongoDB
持久化
为什么需要持久化?
https://redis.io/docs/management/persistence/
因为 Redis 是基于内存的,如果机器宕机就会导致数据丢失,所以需要将数据保存到磁盘。
有哪些持久化方式?
RDB:快照(dump.rdb)
AOF:只记录写命令的日志(appendonly.aof)
RDB的触发条件是什么?
1、自动保存
2、手动保存
3、执行 flushall 或者 flushdb 命令(生成的 dump.rdb 文件是空的)
4、没有开启 AOF 的情况下执行 shutdown 命令
5、主从复制时,主节点触发
[自动保存]
修改 redis.conf 文件,比如 save 60 10000 表示如果 60 秒内至少有 10000 个 key 发生变化就自动保存。
dir 文件路径,dbfilename 文件名称,当 Redis 宕机重启后会自动根据上面的配置进行恢复数据。
所以,Redis 和 dump.rdb 不能在同一台机器上,必须分机隔离,避免物理损坏导致无法恢复。
[手动保存]
可以使用 save 和 bgsave 命令实现,但是生产环境只能使用 bgsave,因为 save 会导致 Redis 阻塞。
lastsave 查看上次保存的时间戳,可以配合 “date -d @时间戳” 使用。
RDB的缺点是什么?
如果还未满足自动保存的条件时 Redis 宕机,那么将会丢失部分数据。
RDB如何修复dump.rdb文件?
使用 redis-check-rdb 命令修复。
RDB如何禁用?
修改 redis.conf 文件:
save ""
AOF缓存区是什么?
相关命令先保存在 AOF 缓存区,再根据同步策略写入 appendonly.aof 文件中,避免频繁的 IO 操作。
AOF有哪些同步策略?
# 同步策略,可选值 always、everysec、no,默认 everysec 即可
appendfsync everysec
always 表示同步写入,需要频繁的 IO 操作,对性能不友好。
everysec 表示每秒写入,即使宕机也只是丢失 1 秒的数据。
no 表示由操作系统决定何时写入,如果宕机将会丢失大量数据,对数据不友好。
AOF的触发条件是什么?
dir 文件路径,appendfilename 文件名称,当 Redis 宕机重启后会自动根据上面的配置进行恢复数据。
AOF的缺点是什么?
相同的数据集 AOF 文件通常远远大于 RDB 文件,数据的恢复速度也慢于 RDB。
AOF如何修复appendonly.aof文件?
使用 redis-check-aof --fix 命令修复。
AOF如何禁用?
appendonly no
AOF重写机制是什么?
压缩,只保留可以恢复数据的最小指令集。
AOF重写机制的触发条件是什么?
自动重写和手动重写。
[自动重写]
由两个参数决定:auto-aof-rewrite-percentage 自动重写的百分比,auto-aof-rewrite-min-size 自动重写的最小大小。
默认值是 100 和 64mb,表示当前文件的大小必须是上次重写后的一倍,并且大于 64mb 时,才会进行自动重写。
[手动重写]
使用 bgrewriteaof 命令。
混合模式
默认情况下,如果同时开启 RDB 和 AOF,那么只会加载 AOF,但是可以通过配置 aof-use-rdb-preamble 实现混合模式。
appendonly.aof 文件的前半部分是 RDB 格式,后半部分是 AOF 格式,也就是说,RDB 实现全量持久化,AOF 实现增量持久化。
集群
高可用的演变过程
主从 -> 哨兵 -> 集群

槽位映射之哈希算法
不利于扩缩容

槽位映射之一致性哈希算法
哈希算法对服务器数量取模,而一致性哈希算法对 2^32 取模。
虽然一致性哈希算法解决了哈希算法不利于扩缩容的问题,但同时也引入了数据倾斜的问题。
为了解决数据倾斜的问题,一致性哈希算法引入了虚拟节点机制。

槽位映射之哈希槽(推荐)
https://redis.io/docs/reference/cluster-spec/#key-distribution-model
哈希槽其实就是一个 [0, 2^14-1] 的数组。
集群的密钥空间被分成 16384 个槽,有效地设置了 16384 个主节点的集群大小上限(但是,建议的最大节点大小约为 1000 个节点)。
System.out.println(io.lettuce.core.cluster.SlotHash.getSlot("K1"));

为什么哈希槽的数量是16384?
https://github.com/redis/redis/issues/2576
2^14=16384,2^16=65536
1、因为 Redis 需要发送心跳数据包,如果槽位是 65536 就需要 8KB,而 16384 只需要 2KB,节约带宽。
2、Redis 建议的最大节点大小约为 1000 个节点,所以没有必要拓展到 65536 个。
如何查看集群信息?
cluster nodes
如何查看Key的槽位?
cluster keyslot k1
如何防止路由失效?
连接时添加 -c 参数:
redis-cli -a redis -p 6379 -c
如果主节点宕机,其对应的从节点会怎么样?
如果当前的主节点宕机,其对应的从节点将会成为新的主节点。
即使原来的主节点恢复,其也只能成为当前主节点的从节点。
如何切换节点的从属关系?
cluster failover
为什么至少需要三个主节点?
因为选举新的主节点需要大于半数的主节点同意才能成功,如果只有两个主节点,当其中一个宕机时将无法达到选举条件。
为什么推荐节点数为奇数?
可以在满足选举条件的基础上节省一个节点。
比如三个主节点和四个主节点相比,如果只有一个主节点宕机都可以选举新的主节点,如果有两个主节点宕机都不能选举新的主节点。
内存淘汰
过期删除
当 key 过期时,Redis 采用 “惰性删除 + 定期删除”。
惰性删除:每次访问 key 时检查是否过期,如果过期则删除。
但是,如果 key 过期后不再访问,其将永远存在,导致内存泄露。
通过 expireIfNeeded 函数实现:
int expireIfNeeded(redisDb *db, robj *key) {
// 如果没有过期不做处理
if (!keyIsExpired(db, key)) return 0;
// 如果 lazyfree_lazy_expire 为 1 表示异步删除,反之同步删除
return server.lazyfree_lazy_expire ? dbAsyncDelete(db, key) : dbSyncDelete(db, key);
}
定期删除:
每隔 100 毫秒随机抽取 20 个 key 检查是否过期,如果过期则删除。
其中,如果[过期 key 的数量] 除以 [抽取 key 的数量] 大于 0.25,则继续抽取。
但是,如果漏掉很多过期 key,其将堆积在内存,导致内存泄露。
如果超过最大内存?
(error) OOM command not allowed when used memory > 'maxmemory'.
当运行内存超过最大内存时,为了避免内存泄露,将会触发内存淘汰策略。
怎么查看内存信息?
# 查看内存信息
info memory
怎么设置最大内存?
修改 redis.conf 文件,最大内存一般设置为物理内存的四分之三,单位 bytes:
maxmemory 104857600
怎么设置内存淘汰策略?
修改 redis.conf 文件,推荐使用 allkeys-lru:
maxmemory-policy allkeys-lru
1、可以保留最近被访问的热点数据,提高缓存的命中率。
2、相对于其它内存淘汰策略,allkeys-lru 适用的场景更多。
有哪些内存淘汰策略?
总共八种。
volatile-lru:使用 LRU 算法进行删除,针对于设置了过期时间的 key 而言。
allkeys-lru:使用 LRU 算法进行删除,针对于所有 key 而言。
volatile-lfu:使用 LFU 算法进行删除,针对于设置了过期时间的 key 而言。
allkeys-lfu:使用 LFU 算法进行删除,针对于所有 key 而言。
volatile-random:随机删除,针对于设置了过期时间的 key 而言。
allkeys-random:随机删除,针对于所有 key 而言。
volatile-ttl:删除即将过期的 key。
noeviction:默认策略,不会删除任何 key,报错 error。
LRU和LFU的区别?
LRU:随机抽取 5 个 key,然后删除最久没有使用的那个。
LFU:删除访问频率最低的 key。
===== =====
下载安装
$
sudo docker pull redis:6.0.6
$
sudo mkdir -p /docker/redis6379/conf /docker/redis6379/data /docker/redis6380/conf /docker/redis6380/data /docker/redis6381/conf /docker/redis6381/data
$
sudo chmod -R 777 /docker/
$
sudo tee /docker/redis6379/conf/redis.conf <<-'EOF'
# 所有 ip 均可访问
bind 0.0.0.0
# 访问密码
requirepass redis
# 文件路径
dir ./
# RDB 文件名称
dbfilename dump.rdb
# 如果 60 秒内至少有 10000 个 key 发生变化就自动保存
save 60 10000
# 如果发生错误则停止写入,避免数据不一致
stop-writes-on-bgsave-error yes
# 使用 LZF 算法进行压缩
rdbcompression yes
# 使用 CRC64 算法进行校验
rdbchecksum yes
# 默认 no 即可
rdb-del-sync-files no
# 开启 AOF
appendonly yes
# AOF 文件名称
appendfilename appendonly.aof
# 同步策略,可选值 always、everysec、no,默认 everysec 即可
appendfsync everysec
# 重写时不要进行同步,避免数据不一致
no-appendfsync-on-rewrite no
# 自动重写的百分比
auto-aof-rewrite-percentage 100
# 自动重写的最小大小
auto-aof-rewrite-min-size 64mb
# 忽略最后一条可能存在问题的指令
aof-load-truncated yes
# 开启混合持久化,先加载 rdb 文件,再加载 aof 文件
aof-use-rdb-preamble yes
# 禁用危险命令
rename-command config ""
rename-command flushall ""
rename-command flushdb ""
rename-command keys ""
rename-command save ""
EOF
$
sudo cp /docker/redis6379/conf/redis.conf /docker/redis6380/conf/
$
sudo cp /docker/redis6379/conf/redis.conf /docker/redis6381/conf/
$
sudo docker run -d \
--name redis6379 \
--restart=always \
--privileged=true \
--sysctl net.core.somaxconn=1024 \
-p 6379:6379 \
-p 26379:26379 \
-v /docker/redis6379/conf/:/etc/redis/ \
-v /docker/redis6379/data:/data \
redis:6.0.6 \
redis-server /etc/redis/redis.conf
$
sudo docker run -d \
--name redis6380 \
--restart=always \
--privileged=true \
--sysctl net.core.somaxconn=1024 \
-p 6380:6379 \
-p 26380:26379 \
-v /docker/redis6380/conf/:/etc/redis/ \
-v /docker/redis6380/data:/data \
redis:6.0.6 \
redis-server /etc/redis/redis.conf
$
sudo docker run -d \
--name redis6381 \
--restart=always \
--privileged=true \
--sysctl net.core.somaxconn=1024 \
-p 6381:6379 \
-p 26381:26379 \
-v /docker/redis6381/conf/:/etc/redis/ \
-v /docker/redis6381/data:/data \
redis:6.0.6 \
redis-server /etc/redis/redis.conf
$
sudo docker restart redis6379 redis6380 redis6381
$
sudo docker exec -it redis6379 /bin/bash
redis-cli
auth redis
$
sudo docker exec -it redis6380 /bin/bash
redis-cli
auth redis
$
sudo docker exec -it redis6381 /bin/bash
redis-cli
auth redis
整合SpringBoot
Jedis、Lettuce、RedisTemplate、Redission 选型:
https://www.jianshu.com/p/786debcbbe8d
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构