Redis

双写一致性

双写模式

对性能要求比较高的业务,可以采用异步双写模式。
对数据要求比较高的业务,可以采用同步双写模式。

更新策略

凌晨停机,使用单线程更新:
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 ;
<!-- Alibaba Canal -->
<!-- https://mvnrepository.com/artifact/com.alibaba.otter/canal.client -->
<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) {
                    // MySQL 没有变动
                    emptyCount++;
                    System.out.println("emptyCount:" + emptyCount);
                    TimeUnit.SECONDS.sleep(1);
                } else {
                    // MySQL 发生变动
                    emptyCount = 0;
                    printEntryAndUpdateRedis(message.getEntries());
                    deleteRedisData();
                }
                canalConnector.ack(id);
                // canalConnector.rollback(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;
}

布隆过滤器

作用

用于判断一个元素是否存在集合中,能够减少内存占用,但是存在哈希冲突。
因为存在哈希冲突,所以只能添加元素,不能删除元素,避免增加误判率。

原理

底层通过一个位数组和多个哈希函数实现。
判断结果:“存在” 不一定存在,“不存在” 一定不存在。

image

GoogleGuava

BloomFilter -> Redis -> MySQL
https://github.com/google/guava/blob/master/guava/src/com/google/common/hash/BloomFilter.java
<!-- Guava 是一套核心和扩展库,包括实用程序类、Google 的集合、I/O 类等等 -->
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<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 实现增量持久化。

集群

高可用的演变过程

主从 -> 哨兵 -> 集群

image

槽位映射之哈希算法

不利于扩缩容

image

槽位映射之一致性哈希算法

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

image

槽位映射之哈希槽(推荐)

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"));

image

为什么哈希槽的数量是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
posted @   linycat  阅读(73)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
点击右上角即可分享
微信分享提示