Redis 学习笔记
在根据“江南一点雨”付费视频学习过程中的做的笔记。想具体了解请在微信搜索“江南一点雨”微信公众号,查找付费视频。
1. Redis 安装
四种方式获取一个Redis:
- 直接编译安装(推荐使用)
- 使用Docker
- 直接安装
- 在线体验
1.1 直接编译安装
在 Linux 系统中提前准备好 gcc 环境
yum install gcc-c++
下载并安装 Redis
# 可通过 redis.io 官网获取最新版本的下载链接(右键下载选项复制链接地址)
wget https://download.redis.io/releases/redis-6.2.4.tar.gz
# 解压下载好的redis压缩包。以下命令将解压到根目录
tar -zxvf redis-6.2.4.tar.gz
#进入解压好的文件
cd redis-6.2.4/
make
make install
安装完成后,启动Redis(此时是在前台启动)
redis-server redis.conf
启动成功页面如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WBMXTShG-1626487794352)(C:\Users\19633\AppData\Roaming\Typora\typora-user-images\image-20210713151655241.png)]
1.5 Redis 启动
以直接编译安装为例
通过 vim 编辑器修改redis.conf
配置文件
[root@localhost ~]# vi redis-6.2.4/redis.conf
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cy917HQh-1626487794354)(C:\Users\19633\AppData\Roaming\Typora\typora-user-images\image-20210713151953112.png)]
启动 Redis,此时就是在后台启动Redis
# 进入Redis安装路径
[root@localhost ~]# cd redis-6.2.4
# 启动 Redis
[root@localhost redis-6.2.4]# redis-server redis.conf
# 使用 Redis
[root@localhost redis-6.2.4]# redis-cli
# 进入使用界面
127.0.0.1:6379>
#退出使用界面
127.0.0.1:6379> exit
2. 五种基本数据类型
2.1 String
常见的命令
关键字 | 介绍 | 使用方式 |
---|---|---|
set | 给一个key赋值 | set key value |
get | 获取一个key的value | get key |
getrange | 返回value的字串,类似substring。-1表示最后一个字符,-2表示倒数第二个字符 | getrange key start end |
getset | 获取并更新key的value(返回旧value,并修改value) | getset key value |
mget | 批量获取 | mget [key…] |
mset | 批量存储 | mset [key value …] |
append | 如果key存在,则在对应的value后追加值,否则就创建新的键值对 | append key value |
decr | 对value减1操作(value必须是数字),如果value不存在,则在0的基础上减1 | decr key |
decrby | 和decr类似,但是可以设置步长 | decrby key decrement(减去的数字) |
incr | 给key的value自增1 | incr key |
incrby | 给key的value自增,并且可以设置步长 | incrby key increment(自增数) |
incrbyfloat | 给key的value自增,可以设置步长为浮点数 | incrbyfloat key increment(自增数) |
ttl | 查看 key 的有效期,-1表示永久 | ttl key |
setex | 给key设置value的同时,还设置过期时间 | setex key seconds(以秒为单位) value |
psetex | 和setex类似,只不过这里的时间单位是毫秒 | psetex key milliseconds value |
setnx | 默认情况下,set命令或覆盖已经存在的key ,但setnx则不会 | setnx key value |
msetnx | 批量设置 | msetnx [key value …] |
strlen | 获取字符串长度 | strlen key |
2.2 BIT
在 Redis 中,字符串都是以二进制的方式来存储的。例如 set k1 a,a 对应的 ASCII 码是 97,97 转为
二进制是 01100001,BIT 相关的命令就是对二进制进行操作的 。
关键字 | 介绍 | 使用方式 |
---|---|---|
getbit | key对应的value在offset处的bit值 | getbit key offset |
setbit | 修改key对应的value在offset处的bit值 | setbit key offset value |
bitcount | 统计二进制数据中1的个数 | bitcount key [start end] |
2.3 LIst
将所有指定的值插入到存于key的列表的头部。如果key 不存在,那么在进行push操作前会创建一个空列表。如果key对应的值不是一个list的话,那么返回一个错误。说白了就是key 存的是list。
关键字 | 介绍 | 使用方式 |
---|---|---|
rpush | 向key的列表尾部插入所有指定的值 | rpush key [element …] (存入的数据) |
lrange | 返回列表指定区间内的元素 | lrange key start stop |
rpop | 移除尾元素并返回列表的尾元素 | rpop key [count](移除的个数) |
lpop | 移除头元素并返回列表的头元素 | lpop key [count](移除的个数) |
lindex | 返回列表中,下标为index的元素 | lindex key index |
ltrim | 可以对一个列表进行修剪 | ltrim key start stop |
blpop | 阻塞式的弹出,相当于 lpop 的阻塞版 | blpop key [key …] timeout |
2.4 Set
关键字 | 介绍 | 使用方式 |
---|---|---|
sadd | 添加元素到一个key中 | sadd key [member …] |
smembers | 获取一个key下的所有元素 | smembers key |
srem | 移除指定的元素 | srem key [member …] |
sismemeber | 判断集合中是否存在member | sismember key member |
scard | 返回集合的数量 | scard key |
srandmember | 随机返回count个元素 | srandmember key [count] |
smove | 把一个元素从一个集合移到另一个集合中 | smove source destination member |
sdiff | 返回两个集合的差集 | sdiff [key …] |
sinter | 返回两个集合的交集 | sinter [key …] |
sdiffstore | 类似sdiff,不同的是,计算出来的结果会保存在一个新的集合中 | sdiffstore destination [key …] |
sunion | 求并集 | sunion [key …] |
sunionstore | 求并集并将结果保存到新的集合中 | sunionstore destination [key …] |
2.5 Hash
在hash结构中,key 是一个字符串,value则是一个 key / value 键值对
关键字 | 介绍 | 使用方式 |
---|---|---|
hset | 添加值 | hset key [ field value …] |
hget | 获取值 | hget key field |
hmset | 批量设置 | hset key [ field value …] |
hmget | 批量获取 | hget key [field …] |
hdel | 删除一个指定的field | hdel key [field …] |
hsetnx | 默认情况下,如果key 和field相同,会覆盖已有的value,hsetnx则不会 | hsetnx key field value |
hvals | 获取所有的value | hvals key |
hkeys | 获取所有的key | hkeys key |
hgetall | 同时获取所有的key和value | hgetall key |
hexists | 判断field是否存在 | hexists key field |
hincrby | 给指定的value自增 | hincrby key field increment |
hincrbyfloat | 可以自增一个浮点数 | hincrbyfloat key field increment |
hlen | 返回key中的value数量 | hlen key |
hstrlen | 返回key中某个field的字符串长度 | hstrlen key field |
2.6 ZSet
关键字 | 介绍 | 使用方式 |
---|---|---|
zadd | 将指定的元素添加到有序集合中 | |
zscore | 返回 member 的 score 值 | |
zrange | 返回集合中的一组元素。 | |
zrevrange | 返回一组元素,但是是倒序。 | |
zcard | 返回元素个数 | |
zcount | 返回 score 在某一个区间内的元素。 | |
zrangebyscore | 按照 score 的范围返回元素。 | |
zrank | 返回元素的排名(从小到大) | |
zrevrank | 返回元素排名(从大到小) | |
zincrby | score 自增 | |
zinterstore | 给两个集合求交集 | |
zrem | 弹出一个元素 | |
zlexcount | 计算有序集合中成员数量 | |
zrangebylex | 返回指定区间内的成员 |
2.7 key
关键字 | 介绍 | 使用方式 |
---|---|---|
del | 删除一个key / value | del [key …] |
dump | 序列化给定的key | dump key |
exists | 判断一个key是否存在 | exists [key …] |
ttl | 查看 key 的有效期,-1表示永久 | ttl key |
expire | 给一个key设置有效期,如果key在过期之前被重新set了,则过期时间会失效 | expire key seconds |
persist | 移除一个key的过期时间 | persits key |
keys * | 查看所有的key | keys * |
pttl | 和ttl一样,只不过这里返回的是毫秒 | pttl key |
2.8 补充
- 四种数据类型(list/set/zset/hash),在第一次使用时,如果容器不存在,就自动创建一个。
- 四种数据类型(list/set/zset/hash),如果里面没有元素了,那么立即删除容器,释放内存。
- 清空redis 缓存 :flushdb / flushall
3. Redis 的 Java 客户端
3.1 开启 Redis 远程连接
修改 redis.conf
配置文件的以下部分:
# 进入 redis.conf 配置文件
[root@localhost ~]# vi redis-6.2.4/redis.conf
# 注释掉该地址,运行所有连接
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#bind 127.0.0.1 -::1
# 开启密码
# The requirepass is not compatable with aclfile option and the ACL LOAD
# command, these will cause requirepass to be ignored.
#
# requirepass foobared
requirepass cenrc
3.2 使用 idea 的 java 客户端连接 Redis
- 创建一个 maven 工程,并引入
jedis
依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.3.0</version>
</dependency>
- 测试连接是否成功
public class MyJedis {
public static void main(String[] args) {
//1. 构造一个 Jedis 对象,默认端口6379可以省略
Jedis jedis = new Jedis("192.168.88.101");
//2. 密码认证,根据在 redis.conf 配置文件中配置的密码
jedis.auth("cenrc");
//3. 测试是否连接成功
String ping = jedis.ping();
//4. 返回 pong 表示连接成功
System.out.println(ping);
}
}
踩坑:
注意是否已经在 Linux 系统开启 Redis 服务
3.3 连接池
在实际应用中,Jdis 实例我们都是通过连接池来获取,由于 Jedis 对象不是线程安全的,所有,当我们使用 Jedis 对象时,从连接池中获取 Jedis ,使用完成之后,再还给连接池。
public class JedisPoolTest {
public static void main(String[] args) {
//1. 构造一个 Jedis 连接池
JedisPool pool = new JedisPool("192.168.88.101", 6379);
//2. 从连接池中获取一个 Jedis 连接
Jedis jedis = pool.getResource();
// 如果设置了密码,则要认证
jedis.auth("cenrc");
//3. Jedis 操作
String ping = jedis.ping();
System.out.println(ping);
//4. 归还连接
jedis.close();
}
}
以上如果在 Jedis 操作时出现异常将导致无法归还 Jedis 连接池
优化写法:
public class JedisPoolTest {
public static void main(String[] args) {
// 构造一个 Jedis 连接池
JedisPool pool = new JedisPool("192.168.88.101", 6379);
// 使用 try 语法糖
try (Jedis jedis = pool.getResource()){
// 如果设置了密码,则要认证
jedis.auth("cenrc");
// Jedis 操作
String ping = jedis.ping();
System.out.println(ping);
}
}
}
对 Jedis 进行进一步封装:
- 创建一个
CallWithJedis
接口,作为参数,
public interface CallWithJedis {
void call(Jedis jedis);
}
- 封装 Redis
public class Redis {
private JedisPool pool;
public Redis() {
GenericObjectPoolConfig config = new GenericObjectPoolConfig();
//连接池最大空闲数
config.setMaxIdle(300);
//连接池最大连接数
config.setMaxTotal(1000);
//连接最大等待时间,-1 表示没有限制
config.setMaxWaitMillis(30000);
/**
* 1. 配置信息
* 2. Redis 地址
* 3. Redis 端口
* 4. 连接超时时间
* 5. 密码
*/
pool = new JedisPool(config,"192.168.88.101",6379,30000,"cenrc");
}
/**
*
* @param callWithJedis 是一个接口,将来传进一个实现类
*/
public void execute(CallWithJedis callWithJedis){
//从连接池中获取连接
try (Jedis jedis = pool.getResource()){
//调用CallWithJedis实现类的 call 方法
callWithJedis.call(jedis);
}
}
}
测试:
public class JedisPoolTest {
public static void main(String[] args) {
Redis redis = new Redis();
// 传进一个 CallWithJedis 接口实现类
redis.execute(jedis -> {
// jedis.auth("cenrc");
String ping = jedis.ping();
System.out.println(ping);
});
}
}
4. Redis 实现分布式锁
4.1 简单实现
分布式锁实现思路很简单,就是进来一个线程先占位,当别的线程进来操作时,发现已经有人占位了,就会放弃或者稍后再试。
在 Redis 中,占用一般使用 setnx 指令(如果不存在key ,则创建key ),先进来的线程先占位,线程的操作执行完成后,在调用 del 指令释放位置。
根据上面的思路,代码如下:
public static void main(String[] args) {
Redis redis = new Redis();
redis.execute(jedis -> {
//占位
Long setnx = jedis.setnx("k1", "v1");
if (setnx == 1){
//没有人占位,则进行操作
String set = jedis.set("name", "crc");
System.out.println(set);
//释放资源
jedis.del("k1");
}else{
//有人占位,停止/暂缓 操作
}
});
}
4.2 优化1:业务代码抛出异常造成死锁
以上代码存在一个小小问题:如果代码业务执行的过程中抛异常或者挂了,这样会导致 del 指令没有被调用,导致 k1 无法被释放,后面来的请求全部堵塞在这里,锁永远得不到释放。
解决方案:给锁添加过期时间,确保一定时间后,能够得到释放
改进代码:
public static void main(String[] args) {
Redis redis = new Redis();
redis.execute(jedis -> {
//占位
Long setnx = jedis.setnx("k1", "v1");
if (setnx == 1){
//给锁添加一个过期时间,防止应用在运行过程中抛出异常导致锁无法及时得到释放
jedis.expire("k1",5);
//没有人占位,则进行操作
String set = jedis.set("name", "crc");
System.out.println(set);
//释放资源
jedis.del("k1");
}else{
//有人占位,停止/暂缓 操作
}
});
}
4.3 优化2:不具备原子可能导致死锁
以上代码还存在一个问题:由于 setnx 指令和 expire 指令是两个操作,不具备原子性,也可能导致死锁。
解决方案:将两个指令操作 合在一起,同时执行
优化代码:
public static void main(String[] args) {
Redis redis = new Redis();
redis.execute(jedis -> {
//占位
// Long setnx = jedis.setnx("k1", "v1");
String s = jedis.set("k1", "v1", new SetParams().nx().ex(5));
if (s != null && "OK".equals(s)){
//给锁添加一个过期时间,防止应用在运行过程中抛出异常导致锁无法及时得到释放
// jedis.expire("k1",5);
//没有人占位,则进行操作
String set = jedis.set("name", "crc");
System.out.println(set);
//释放资源
jedis.del("k1");
}else{
//有人占位,停止/暂缓 操作
}
});
}
4.4 优化3:解决超时问题带来的混乱
目前还存在一个超时问题:我们为了防止业务代码在执行时抛出异常给每一个锁添加了一个超时时间,超时之后锁会自动释放。也就导致了有可能业务代码还未执行完资源就被释放了,这时,其他线程就有可能占用了该锁。当第一个线程执行完成后再一次释放该锁。此时释放的是另一个线程占用的锁。如此循环,造成混乱。
解决方案:
- 尽量避免在获取锁之后,执行耗时操作。
- 可以在锁上面做文章,将锁的 value 设置为一个随机字符串,每次释放锁的时候,都去比较随机字符串是否一致,如果一致,再去是否,不释放。
针对第二种解决方案:由于释放锁的时候,第一步要去查看锁的 value ,第二步比较 value 的值是否正确,第三步释放锁,很明显这三个步骤不具备原子性,为了解决这个问题,我们得引入Lua 脚本。
在 Redis 中,使用 Lua 脚本,大致上又两种思路:
- 提前在 Redis 服务端写好 Lua 脚本,然后在 Java 客户端去调用脚本(推荐)
- 可以直接在 Java 端写 Lua 脚本,写好之后,需要执行时,每次将脚本发送到 Redis 上执行
第一种思路:
可以在 Redis 安装目录下创建专门存放 Lua 脚本的文件夹
[root@localhost ~]# mkdir redis-6.2.4/Lua
使用 vim 编辑器创建 Lua 脚本
# 创建名为releasewherevalueequel.lua 的脚本
[root@localhost lua]# vi releasewherevalueequel.lua
# 脚本内容
if redis.call("get",KEYS[1])==ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
可以给 Lua 脚本求一个 SHA1和 ,命令如下:
# 传输的文件:lua/releasewherevalueequel.lua
# 传输到: redis-cli
# 缓存:script load
[root@localhost redis-6.2.4]# cat lua/releasewherevalueequel.lua | redis-cli -a cenrc script load --pipe
# 返回结果
"b8059ba43af6ffe8bed3db65bac35d452f8115d8"
script load这个命令会在 Redis 服务器中缓存 Lua脚本,并返回脚本内容的 SHA1 校验和(脚本文件的唯一标识),然后在 Java 端调用时,传入 SHA1 校验和作为参数,这样 Redis 服务器就知道执行那个脚本了。
实现代码:
public static void main(String[] args) {
Redis redis = new Redis();
redis.execute(jedis -> {
//1. 先获取一个随机字符串
String value = UUID.randomUUID().toString();
//2. 获取锁
String k1 = jedis.set("k1", value, new SetParams().nx().ex(5));
//3. 判断是否拿到锁
if (k1 != null && "OK".equals(k1)){
//4. 具体的业务
jedis.set("name","yitianwuliao");
String name = jedis.get("name");
System.out.println(name);
//5. 调用 Lua 脚本判断释放锁
jedis.evalsha("b8059ba43af6ffe8bed3db65bac35d452f8115d8", Arrays.asList("k1"),Arrays.asList(value));
}else {
System.out.println("没拿到锁");
}
});
}
可以在 Redis 服务端查看是否还存在 k1
127.0.0.1:6379> keys *
5. Redis 实现消息队列
5.1 消息队列
Redis 做消息队列,使用它里边的 List 数据结构就可以实现,我们可以使用 lpush/rpush 操作来实现入队,然后使用 lpop/rpop 来实现出队。在客户端(例如 Java 端),我们会维护一个死循环来不停的从队列中读取消息,并处理,如果队列中有消息,则直接获取到,如果没有消息,就会陷入死循环,直到下一次有消息进入,这种死循环会造成大量的资源浪费,这个时候,我们可以使用之前讲的blpop/brpop 。
5.2 延迟消息队列
延迟队列可以通过 zset 来实现,因为 zset 中有一个 score ,我们可以把时间作为 score ,将 value 存到 Redis 中,然后通过轮询的方式,去不断的读取消息出来。
首先,如果消息是一个字符串,直接发送即可,如果是一个对象,则需要对对象进行序列化,因为 Redis 存的是字符串,无法存对象。
这里使用 JSON 来实现序列化和反序列化。
加入 JSON 依赖
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.3</version>
</dependency>
- 构造一个消息对象
public class Message {
//消息对象的唯一标识
private String id;
//消息内容
private Object data;
@Override
public String toString() {
return "Message{" +
"id='" + id + '\'' +
", data=" + data +
'}';
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
- 封装延迟消息队列
public class DelayMsgQueue {
// jedis 连接
private Jedis jedis;
// 延迟消息队列的唯一标识(key)
private String queue;
public DelayMsgQueue(Jedis jedis, String queue) {
this.jedis = jedis;
this.queue = queue;
}
/**
* 消息入队
* @param data 将要发送的消息
*/
public void queue(Object data){
//将传入的消息封装到消息对象中
Message msg = new Message();
msg.setId(UUID.randomUUID().toString());
msg.setData(data);
//因为 Redis 无法存储对象,因此对消息对象进行序列化
try {
String s = new ObjectMapper().writeValueAsString(msg);
//当前时间
System.out.println("消息入队:" + new Date());
//消息发送,score 延迟 5 秒
jedis.zadd(queue,System.currentTimeMillis()+5000,s);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
public void loop(){
//Thread.interrupted()判断线程是否中断
while (!Thread.interrupted()){
//读取 score 在 0 到当前时间戳之间的消息。
Set<String> zrange = jedis.zrangeByScore(queue, 0, System.currentTimeMillis(), 0, 1);
//如果消息是空的,则休息 500 毫秒然后继续
if (zrange.isEmpty()){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
break;
}
continue;
}
String next = zrange.iterator().next();
//弹出一个元素
if (jedis.zrem(queue,next) > 0){
try {
//将读取到的消息进行反序列化
Message msg = new ObjectMapper().readValue(next, Message.class);
System.out.println("消息消费:" + msg);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
}
}
- 测试
public static void main(String[] args) {
Redis redis = new Redis();
redis.execute(jedis -> {
//构造一个消息队列
DelayMsgQueue queue = new DelayMsgQueue(jedis, "yitianwuliao-queue");
//构造消息生产者
Thread producer = new Thread(){
@Override
public void run() {
for (int i = 0; i < 5; i++){
//存入的消息
queue.queue("crc >>> " + i);
}
}
};
//构造消息消费者
Thread consumer = new Thread(){
@Override
public void run() {
queue.loop();
}
};
producer.start();
consumer.start();
try {
//休息 7 秒后,停止程序(因为在消息消费中设置的循环为死循环,需要手动停止)
Thread.sleep(7000);
consumer.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
测试结果:
消息入队:Thu Jul 15 11:10:50 CST 2021
消息入队:Thu Jul 15 11:10:50 CST 2021
消息入队:Thu Jul 15 11:10:50 CST 2021
消息入队:Thu Jul 15 11:10:50 CST 2021
消息入队:Thu Jul 15 11:10:50 CST 2021
消息消费:Message{id='53e643b8-aab9-49e3-a798-663c4db718fa', data=crc >>> 0}
消息消费:Message{id='447e85d1-d004-4dce-825a-146bf72a9582', data=crc >>> 1}
消息消费:Message{id='71c296f0-3759-4ba4-9f89-064ba412e970', data=crc >>> 2}
消息消费:Message{id='fefbcfcf-ac6e-41d9-93fa-6432a2baab5c', data=crc >>> 3}
消息消费:Message{id='5ac3e38f-baa2-417a-b967-92e2c5b5e916', data=crc >>> 4}
6. Redis 中 Bit操作
6.1 实现签到统计
用户一年的签到记录,如果你用 string 类型来存储,那你需要 365 个 key/value,操作起来麻烦。通过位图可以有效的简化这个操作。
它的统计很简单:
01111000111
每天的记录占一个位,365 天就是 365 个位,大概 46 个字节,这样可以有效的节省存储空间,如果有一天想要统计用户一共签到了多少天,统计 1 的个数即可。对于位图的操作,可以直接操作对应的字符串(get/set),可以直接操作位(getbit/setbit) 。
6.2 基本操作
- 零存整取
通过 setbit 指令实现零存整取:
注意如果是多个字符添加,则是从左往右计算,比如第二个字符的第一位为 8 ,第二位为 9 ,第三位为 10 ,依次类推…
127.0.0.1:6379> setbit name 0 1
(integer) 0
127.0.0.1:6379> setbit name 1 1
(integer) 0
127.0.0.1:6379> setbit name 2 0
(integer) 0
127.0.0.1:6379> setbit name 3 0
(integer) 0
127.0.0.1:6379> setbit name 4 1
(integer) 0
127.0.0.1:6379> setbit name 5 0
(integer) 0
127.0.0.1:6379> setbit name 6 1
(integer) 0
127.0.0.1:6379> setbit name 7 0
(integer) 0
127.0.0.1:6379> get name
"J"
- 整存零取
存一个字符串进去,但是通过位操作获取字符串。
6.3 统计
例如签到记录
0111010010101
1表示签到,0表示没有签到,统计总的签到天数可以使用 bitcount
127.0.0.1:6379> BITCOUNT name
(integer) 4
注意如果使用指定范围,命令中的起始和结束位置都是字符索引,不是 bit 索引。
7. HyperLogLog
一般评估一个网站的访问量,有几个重要的参数:
- pv ,Page View ,网页的浏览量
- uv ,User View ,访问的用户
其中 uv 需要进行去重操作,HyperLogLog 提供了一套不怎么精确但是够用的去重方案,会有误差,官方给出的误差数据是 0.81% 。
HyperLogLog 主要提供两个命令:
- pfadd :用来添加记录,类似 sadd ,添加过程中,重复的记录会自动去重。
- pfcount:用来统计数据。
测试代码:
public static void main(String[] args) {
Redis redis = new Redis();
redis.execute(jedis -> {
for (int i = 0; i < 1000; i++){
jedis.pfadd("uv","u"+i, "u"+(i+1));
}
long uv = jedis.pfcount("uv");
//理论值是1001
System.out.println(uv);
});
}
输出结果是:994
8. 布隆过滤器
8.1 布隆过滤器介绍
主要功能实现消息推送,如今日头条,推送的内容有相似的,但是没有重复的。
布隆过滤器主要实现了判断容器中是否存在该元素。但是,布隆过滤器同样存在误差,特别的是当布隆过滤器返回消息说元素存在,表示元素可能存在(具体原理请看松哥笔记)。当返回消息说元素不存在 ,表示元素一定不存在。
8.2 布隆过滤器安装
两种安装方式
- Docker:
docker run -p 6379:6379 --name redis-redisbloom redislabs/rebloom:latest
- 自己编译安装:
cd redis-5.0.7
git clone https://github.com/RedisBloom/RedisBloom.git
cd RedisBloom/
make
cd ..
redis-server redis.conf --loadmodule ./RedisBloom/redisbloom.so
安装完成后,执行 bf.add 命令,测试安装是否成功。每次启动时都输入 redis-server redis.conf --loadmodule ./RedisBloom/redisbloom.so
比较麻烦,我们可以将要加载的模块在 redis.conf 中提前配置好。
################################## MODULES #####################################
# Load modules at startup. If the server is not able to load modules
# it will abort. It is possible to use multiple loadmodule directives.
#
# loadmodule /path/to/my_module.so
# loadmodule /path/to/other_module.so
loadmodule /root/redis-5.0.7/RedisBloom/redisbloom.so
最下面这一句,配置完成后,以后只需要 redis-server redis.conf 来启动 Redis 即可。
8.3 基本用法
主要是两类命令,添加和判断是否存在。
- bf.add\bf.madd 添加和批量添加
- bf.exists\bf.mexists 判断是否存在和批量判断
在 Java 客户端使用布隆过滤器,添加以下依赖:
<dependency>
<groupId>com.redislabs</groupId>
<artifactId>jrebloom</artifactId>
<version>1.2.0</version>
</dependency>
测试:
public static void main(String[] args) {
GenericObjectPoolConfig config = new GenericObjectPoolConfig();
config.setMaxIdle(300);
config.setMaxTotal(1000);
config.setMaxWaitMillis(30000);
config.setTestOnBorrow(true);
JedisPool pool = new JedisPool(config, "192.168.91.128", 6379, 30000,"javaboy");
Client client = new Client(pool);
//存入数据
for (int i = 0; i < 100000; i++) {
client.add("name", "javaboy-" + i);
}
//检查数据是否存在
boolean exists = client.exists("name", "javaboy-9999999");
System.out.println(exists);
}
9. Redis 持久化
9.1 快照方式
在Redis中,默认情况下,快照持久化的方式就是开启的。
默认情况下会生成一个 dump.rdb 文件,这个文件就是备份下来的文件。当Redis启动时,会自动去加载这个 rdb 文件,从该文件中恢复数据。
在 redis.conf 配置文件中,参数配置:
# 表示快照的频率,第一个表示3600秒内如果有一个键被修改了,则进行快照
save 3600 1
save 300 100
save 60 10000
# 快照执行出错后,是否继续处理客户端的写命令
stop-writes-on-bgsave-error yes
# 表示生成的快照文件名
dbfilename dump.rdb
# 是否对快照文件进行压缩
rdbcompression yes
# 表示生成的快照文件位置
dir ./
备份流程:
- 可以使用 save 命令创建快照(阻塞命令,使用的不多)。
- 使用 bgsave 命令(不影响父进程处理客户端请求)。
- 满足定义的备份规则,会自动触发 bgsave 命令。
- 当执行 shutdown 命令,也会触发 bgsave 命令。
- 主从复制时,当主机接收到从机发来的 sync 同步命令时,首先会触发 bgsave 命令,在给从机发送快照数据进行同步。
9.2 AOF日志
与快照持久化不同,AOF 持久化是将被执行的命令追加到 aof 文件末尾,在恢复时,只需要吧记录下来的命令从头到尾执行一遍即可。
默认情况下,AOF 是没有开启的。我们需要手动开启:
# 开启 aof 配置
appendonly yes
# 表示生成的 AOF 文件名
appendfilename "appendonly.aof"
# 备份的时机
appendfsync everysec
# 表示 aof 文件在压缩时,是否还继续进行同步操作
no-appendfsync-on-rewrite no
# 表示当目前 aof 文件大小超过上一次重写时的 aof 文件大小的百分之多少的时候,再次进行重写
auto-aof-rewrite-percentage 100
# 如果之前没有重写过,则以启动时的 aof 大小为依据,同时要求 aof 文件至少要大于 64M
auto-aof-rewrite-min-size 64mb
同时,为了避免快照备份的影响,记得将快照备份关闭:
save ""
#save 3600 1
#save 300 100
#save 60 10000
10. Redis 主从同步
10.1 CAP
在分布式环境下,CAP 原理是一个非常基础的东西,所有的分布式存储系统,都只能在 CAP 中选择两项实现。
C:consistent 一致性
A:availability 可用性
P:partition tolerance 分布式容忍性
在分布式系统中,这三个只能满足两个:P 肯定要实现的(不实现还是分布式吗),C 和 A 只能选择其中一个。大部分情况下,大多数网站架构选择了 AP 。
在 Redis 中,实际上就是保证最终一致性。
Redis 中,当搭建了主从服务之后,如果主从之间的连接断开了,Redis 依然是可以操作的,相当于它们满足可用性,但是此时主从两个节点中的数据会有差异,相当于牺牲了一致性。但是 Redis 保证最终一致性,就是说当网络恢复的时候,从机会追赶主机,尽量保持数据一致。
10.2 主从复制
由于设备性能有限,无法创建三台虚拟机进行模拟。
使用一台虚拟机进行模拟。启动三个不同的 Redis 服务端口模拟三台不同的主机。地址如下:
192.168.88.101:6379
192.168.88.101:6380
192.168.88.101:6381
- 在 Redis 安装目录创建一个文件夹存放将要启动的三个 Redis服务端口配置文件。
# 进入 Redis 安装目录
cd # cd redis-6.2.4
# 创建文件夹
mkdir ./master-slave
# 将 redis.conf 配置文件复制到创建好的文件夹,并更名为 redis6379.conf、redis6380.conf、redis6381.conf
cp ./redis.conf ./master-slave/redis6379.conf
- 打开 redis6379.conf 将如下配置均加上6379以便区分,(默认是6379的不用修改)。
port 6379
pidfile /var/run/redis_6379.pid
logfile "6379.log"
dbfilename dump6379.rdb
appendfilename "appendonly6379.aof"
- 同理,分别打开 redis6380.conf、redis6381.conf 两个配置文件。同时修改配置文件,将来 6380 和 6381 作为从机,需要添加以下配置:
# 配置主机的密码,否则无法获取数据
masterauth cenrc
- 启动 三个 Redis 服务
[root@localhost redis-6.2.4]# redis-server redis6379.conf
[root@localhost redis-6.2.4]# redis-server redis6380.conf
[root@localhost redis-6.2.4]# redis-server redis6381.conf
- 输入如下命令,分别进入三个实例的控制台
[root@localhost redis-6.2.4]# redis-cli -p 6379 -a cenrc
[root@localhost redis-6.2.4]# redis-cli -p 6380 -a cenrc
[root@localhost redis-6.2.4]# redis-cli -p 6381 -a cenrc
- 查看以下命令,可以看到每个实例默认都是 master
INFO replication
- 配置主从关联。其中,6379 作为主机,6380 和 6381 作为从机。
SLAVEOF 129.0.0.1 6379
- 再次查看每个实例的状态可以看到,6379 为 master,6380 和 6381 为 slave
主机状态:
127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6380,state=online,offset=2261,lag=1
slave1:ip=127.0.0.1,port=6381,state=online,offset=2261,lag=1
master_failover_state:no-failover
master_replid:50a4bdc85561e09094a3b72a033385e8899a0ebe
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:2261
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:2261
从机状态:
127.0.0.1:6381> INFO replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:8
master_sync_in_progress:0
slave_repl_offset:2219
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:50a4bdc85561e09094a3b72a033385e8899a0ebe
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:2219
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:57
repl_backlog_histlen:2163
10.3 哨兵模式
监控主机,当主机挂掉时,会自动从从机选举出新的主机。并且,当原来的主机重新连接后会成为从机。
修改 sentinel.conf 配置文件:
# 其中 mymaster 表示哨兵名称,127.0.0.1 6379 表示监控的主机,1 表示有多少个 sentinel 认为主机挂掉了,就进行选举新的主机
sentinel monitor mymaster 127.0.0.1 6379 1
启动哨兵:
redis-sentinel sentinel.conf
11. Redis 集群
11.1 单虚拟机模拟集群环境搭建:
在 Redis 安装目录下创建 /redis-cluster文件夹,并在文件夹中创建 7001、7002、7003、7004、7005、7006等 6 个文件夹。
将 redis.conf 配置文件分别复制到 创建好的 6 个文件夹,这 6 个配置文件分别代表 6 台不同的节点。
6 个配置文件,分别修改配置文件配置信息如下:
# 表示 Redis服务在后台运行
daemonize yes
# 开启集群
cluster-enabled yes
cluster-config-file nodes-7001.conf
# 集群连接超时时间
cluster-node-timeout 15000
# 端口
port 7001
# 启动 AOF 模式
appendonly yes
# 修改成局域网 ip
bind 192.168.88.101
# 修改密码
requirepass cenrc
# 修改主从密码
masterauth cenrc
分别以 6 个不同的配置文件启动 Redis 服务:
# 使用修改好的配置文件启动 Redis服务
[root@localhost redis-6.2.4]# redis-server ./redis-cluster/7001/redis.conf
[root@localhost redis-6.2.4]# redis-server ./redis-cluster/7002/redis.conf
[root@localhost redis-6.2.4]# redis-server ./redis-cluster/7003/redis.conf
[root@localhost redis-6.2.4]# redis-server ./redis-cluster/7004/redis.conf
[root@localhost redis-6.2.4]# redis-server ./redis-cluster/7005/redis.conf
[root@localhost redis-6.2.4]# redis-server ./redis-cluster/7006/redis.conf
# 查看 Redis 启动的端口
ps -aux|grep redis
# 创建集群,--cluster-replicas 1 表示有一个主机对应一个从机,-a 表示密码
redis-cli --cluster create 192.168.88.101:7001 192.168.88.101:7002 192.168.88.101:7003 192.168.88.101:7004 192.168.88.101:7005 192.168.88.101:7006 --cluster-replicas 1 -a cenrc
# 使用集群:-p 表示启动端口,-c 表示以集群的方式启动,-h 表示bind 的地址
redis-cli -p 7001 -c -h 192.168.88.101
# 查看集群的状态
cluster info
# 查看整个集群的状态
CLUSTER NODES
11.2 动态添加新的节点
创建7007节点,
# 将新节点加入到集群中,第一个地址为新节点地址和端口,第二个地址为集群中国某个节点的地址和端口
redis-cli --cluster add-node 192.168.88.101:7007 192.168.88.101:7006 -a cenrc
# 登录集群
redis-cli -p 7001 -c -h 192.168.88.101
# 查看整个集群的状态
CLUSTER NODES
可以发现增加了新的节点作为 master,但是目前节点并没有被分配槽。
192.168.88.101:7001> CLUSTER NODES
8b02314c4f37e1af61d25665f3d387f725ae6342 192.168.88.101:7002@17002 master - 0 1626483937121 2 connected 5461-10922
7d459a3a029e586589153e9674bf5a60d1732a7b 192.168.88.101:7006@17006 slave bd8b11965d3388565269a03e4c6ca91fb32ce0ad 0 1626483933116 3 connected
c4f4e70479a0c80d74c07f31168ae75719533eaf 192.168.88.101:7001@17001 myself,master - 0 1626483934000 1 connected 0-5460
bd8b11965d3388565269a03e4c6ca91fb32ce0ad 192.168.88.101:7003@17003 master - 0 1626483935118 3 connected 10923-16383
b2e72feeaf54532d4e487a6b69ac92b637f88bc1 192.168.88.101:7005@17005 slave 8b02314c4f37e1af61d25665f3d387f725ae6342 0 1626483936000 2 connected
a5aaa90daa14bab26bcadb3d211336ecc30b638f 192.168.88.101:7007@17007 master - 0 1626483937000 0 connected
7c59b963e3d9bf27d0c4c668151afc9df2daaa9f 192.168.88.101:7004@17004 slave c4f4e70479a0c80d74c07f31168ae75719533eaf 0 1626483935000 1 connected
为新节点分配槽
# 192.168.88.101:7001 表示集群的连接地址(从集群中随便选一个)
# --cluster-from 表示从哪个节点中分配出新的槽,如果从多个节点中分配可以使用 “,” 进行分隔
# --cluster-to 表示将槽分配给哪个节点
# --cluster-slots 表示分配出的槽的数量
# -a 表示密码
redis-cli --cluster reshard 192.168.88.101:7001 --cluster-from c4f4e70479a0c80d74c07f31168ae75719533eaf --cluster-to a5aaa90daa14bab26bcadb3d211336ecc30b638f --cluster-slots 1024 -a cenrc
至此,新节点已经成功加入集群中,并且分配槽。
不过,目录新节点并没有从机。为新节点添加从机也很简单,只需要再添加一台从机节点,在使用添加从机节点命令时,在命令中添加 --cluster-slave --cluster-master-id
。剩下步骤重复以上步骤即可。