redis服务梳理

219 阅读9分钟

Windows服务安装

Redis-x64-3.0.504.msi 网盘地址: www.aliyundrive.com/s/aU5g4ET5d… image.png

启动服务

redis-server.exe redis.windows.conf

image.png

启动测试端

redis-cli.exe -h 127.0.0.1 -p 6379

image.png

Linux安装

第一步:解压

#tar zxvf  redis-6.2.7.tar.gz #cd  redis-6.2.7/

第二步:编译安装

#make #make install

第三步:依赖安装

#yum -y install gcc
#yum -y install gcc-c++

启动服务

src/redis-server redis.conf

配置允许远程连接

在 redis.conf 中修改以下参数

# 允许任何主机连接、访问
bind 127.0.0.1 改为 bind 0.0.0.0
# 关闭保护模式
protected-mode yes 改为 protected-mode no
# 允许启动后在后台运行,即关闭命令行窗口后仍能运行
daemonize no 改为 daemonize yes

集群搭建

官网地址:redis.io/docs/manual…

集群规划

image.png

准备好配置文件

16379、16380修改以下备注的配置

################################## NETWORK #####################################
#绑定本机IP
bind 192.168.137.150
################################# GENERAL #####################################
#集群密码
requirepass tiger
#绑定本机IP
bind 192.168.137.150
#端口号
port 16379
#开启集群模式
cluster-enabled yes
#设置集群模式config文件(是集群自动创建,用于记录集群节点信息以及持久化参数)
cluster-config-file "nodes-16379.conf"
#设置让redis开启后可以在后台运行
daemonize yes
#保存redis的pid,这是默认路径,可自行修改
pidfile "/var/run/redis_16379.pid"
#保存日志文件
logfile "/opt/redis-server/16379/log/redis.log"
#本地数据库存放路径
dir "/opt/redis-server/16379/data"

配置完成后,通过客户端链接测试下ping,是否能够正常工作;

其他集群配置参数

cluster-enabled<yes/no>:如果是,则在特定 Redis 实例中启用 Redis Cluster 支持。否则,实例将像往常一样作为独立实例启动。

cluster-config-file:请注意,尽管有此选项的名称,但这不是用户可编辑的配置文件,而是 Redis Cluster 节点在每次发生更改时自动持久化集群配置(基本上是状态)的文件,为了能够在启动时重新读取它。该文件列出了集群中的其他节点、它们的状态、持久变量等内容。由于某些消息接收,此文件通常会被重写并刷新到磁盘上。

cluster-node-timeout:Redis 集群节点不可用的最长时间,而不被视为失败。如果主节点在超过指定的时间内无法访问,它将由其副本进行故障转移。此参数控制 Redis Cluster 中的其他重要内容。值得注意的是,在指定时间内无法到达大多数主节点的每个节点都将停止接受查询。

cluster-slave-validity-factor:如果设置为零,则副本将始终认为自己有效,因此将始终尝试故障转移主节点,而不管主节点和副本之间的链接保持断开连接的时间长短。如果该值为正,则计算最大断开时间作为节点超时值乘以此选项提供的因子,如果节点是副本,则如果主链接断开连接的时间超过指定的时间,它将不会尝试启动故障转移。例如,如果节点超时设置为 5 秒,有效性因子设置为 10,则与主节点断开连接超过 50 秒的副本将不会尝试对其主节点进行故障转移。请注意,如果没有能够对其进行故障转移的副本,则任何非零值都可能导致 Redis 集群在主故障后不可用。在这种情况下,只有当原始主节点重新加入集群时,集群才会恢复可用。

cluster-migration-barrier:master 将保持连接的最小副本数,以便另一个副本迁移到不再被任何副本覆盖的 master。有关更多信息,请参阅本教程中有关副本迁移的相应部分。

cluster-require-full-coverage<yes/no>:如果设置为 yes,默认情况下,如果某个百分比的键空间未被任何节点覆盖,集群将停止接受写入。如果该选项设置为 no,即使只能处理有关键子集的请求,集群仍将提供查询服务。

cluster-allow-reads-when-down<yes/no>:如果设置为 no,默认情况下,当集群被标记为失败时,Redis 集群中的节点将停止提供所有流量,或者当节点无法到达时达到法定人数或未达到完全覆盖时。这可以防止从不知道集群更改的节点读取可能不一致的数据。可以将此选项设置为 yes 以允许在故障状态期间从节点读取,这对于希望优先考虑读取可用性但仍希望防止写入不一致的应用程序很有用。当使用只有一个或两个分片的 Redis 集群时,也可以使用它,因为它允许节点在主节点失败但无法自动故障转移时继续提供写入服务。

克隆2个虚拟机

分布修改为静态IP,151和152;
遇到克隆虚拟机,可能会存在设置静态IP不生效情况,解决方法见linux常见命令;
将3个节点服务都启动起来;

开放端口

开放服务端口16379、16380和集群通信端口26379、26380(集群端口);

#firewall-cmd --zone=public --add-port=26379/tcp --permanent
#firewall-cmd --zone=public --add-port=26380/tcp --permanent
#firewall-cmd --reload 如果不开放端口执行执行建立集群命令,会一直wait....; 如果你已经中招(我踩坑了),就把每个节点下数据文件目录日志目录下的文件清除掉就可以重新执行建立集群命令了;

建立集群

建立集群命令:

src/redis-cli --cluster create 192.168.137.150:16379 192.168.137.150:16380 192.168.137.151:16379 192.168.137.151:16380 192.168.137.152:16379 192.168.137.152:16380 --cluster-replicas 1

测试集群

执行get name命令,此时由151:16379执行 image.png

通过kill 掉服务模拟151:16379服务中断: image.png

再执行get name命令时,已经由152:16380执行了 image.png

查看集群节点信息,151的16379服务断连了,从服务152的16380服务称为master; image.png

重新启动 151 的16379服务,此时查询集群信息16379变为slave了。 image.png

集群节点的变化记录
image.png

测试集群的健康状况

src/redis-cli -a tiger --cluster check masterIP:port image.png

高级主题之布隆过滤器(BloomFilter)

RedisBloom安装

获取源码

#git clone github.com/RedisBloom/… 查看远程最新的版本分支 #git branch -r 检出远程版本分支 #git checkout -b 2.2 origin/2.2 查看当前分支(带*的为当前分支名称) #git branch 编译 #make

如果出现如下错误,则使用make CFLAGS=-std=c99 命令进行编译,编译完成后会生成一个rebloom.so文件。
image.png

重启redis服务

#src/redis-server ./redis.conf  --loadmodule /usr/local/redis/RedisBloom/redisbloom.so

BloomFilter 相关操作

先来熟悉一下布隆过滤器基本指令:

  • bf.add 添加元素到布隆过滤器;
  • bf.exists 判断元素是否在布隆过滤器;
  • bf.madd 添加多个元素到布隆过滤器,bf.add只能添加一个(格式:bf.madd key v1 v2);
  • bf.mexists 判断多个元素是否在布隆过滤器;

布隆bf.reserve设置

命令bf.reserve,提供了三个参数, key, error_rate和initial_size。错误率越低,需要的空间越大,initial_size参数表示预计放入布隆过滤器的元素数量,当实际数量超出这个数值时,误判率会上升。 默认的参数是 error_rate=0.01, initial_size=100。

测试

image.png

image.png

SpringBoot使用布隆过滤器

/**
 * 布隆过滤器实现
 */
@Test
public void bloom() {
    String bloomKey = "bloomKey";
    Long expectedInsertions = 1000000L;
    Double falseProbability = 0.01;//百分之一的错误率
    boolean createFlag = bloomFilterHelper.tryInitBloomFilter(bloomKey, expectedInsertions, falseProbability);
    ThreadSleepUtils.log("创建bloom过滤器结果:" + createFlag);
    boolean existFlag = bloomFilterHelper.existInBloomFilter(bloomKey, "key1");
    ThreadSleepUtils.log("检查是否存在:" + existFlag);
    bloomFilterHelper.addInBloomFilter(bloomKey, "key1");
    boolean existFlag1 = bloomFilterHelper.existInBloomFilter(bloomKey, "key1");
    ThreadSleepUtils.log("检查是否存在:" + existFlag1);
    redisTemplate.delete(bloomKey);
}
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.data.redis.core.script.RedisScript;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

@Component
public class BloomFilterHelper {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    // 初始化一个布隆过滤器
    public Boolean tryInitBloomFilter(String key, long expectedInsertions, double falseProbability) {
        Boolean keyExist = redisTemplate.hasKey(key);
        if(keyExist) {
            return false;
        }
        RedisScript<Boolean> script = new DefaultRedisScript<>(bloomInitLua(), Boolean.class);
        RedisSerializer stringSerializer = redisTemplate.getStringSerializer();
        redisTemplate.execute(script, stringSerializer, stringSerializer, Collections.singletonList(key), falseProbability+"", expectedInsertions+"");
        return true;
    }
    // 添加元素
    public Boolean addInBloomFilter(String key, Object arg) {
        RedisScript<Boolean> script = new DefaultRedisScript<>(addInBloomLua(), Boolean.class);
        return (Boolean) redisTemplate.execute(script, Collections.singletonList(key), arg);
    }
    // 批量添加元素
    public Boolean batchAddInBloomFilter(String key, Object... args) {
        RedisScript<Boolean> script = new DefaultRedisScript<>(batchAddInBloomLua(), Boolean.class);
        return (Boolean) redisTemplate.execute(script, Collections.singletonList(key), args);
    }
    // 查看某个元素是否是存在
    public Boolean existInBloomFilter(String key, Object arg) {
        RedisScript<Boolean> script = new DefaultRedisScript<>(existInBloomLua(), Boolean.class);
        return (Boolean) redisTemplate.execute(script, Collections.singletonList(key), arg);
    }
    // 批量查看元素是否存在
    public List batchExistInBloomFilter(String key, Object... args) {
        RedisScript<List> script = new DefaultRedisScript(batchExistInBloomLua(), List.class);
        List<Long> results = (List) redisTemplate.execute(script, Collections.singletonList(key), args);
        List<Boolean> booleanList = results.stream().map(res -> res == 1 ? true : false).collect(Collectors.toList());
        return booleanList;
    }


    private String bloomInitLua() {
        return "redis.call('bf.reserve', KEYS[1], ARGV[1], ARGV[2])";
    }
    private String addInBloomLua() {
        return "return redis.call('bf.add', KEYS[1], ARGV[1])";
    }
    private String batchAddInBloomLua() {
        StringBuilder sb = new StringBuilder();
        sb.append("for index, arg in pairs(ARGV)").append("\r\n");
        sb.append("do").append("\r\n");
        sb.append("redis.call('bf.add', KEYS[1], arg)").append("\r\n");
        sb.append("end").append("\r\n");
        sb.append("return true");
        return sb.toString();
    }
    private String existInBloomLua() {
        return "return redis.call('bf.exists', KEYS[1], ARGV[1])";
    }
    private String batchExistInBloomLua() {
        StringBuilder sb = new StringBuilder();
        sb.append("local results = {}").append("\r\n");
        sb.append("for index, arg in pairs(ARGV)").append("\r\n");
        sb.append("do").append("\r\n");
        sb.append("local exist = redis.call('bf.exists', KEYS[1], arg)").append("\r\n");
        sb.append("table.insert(results, exist)").append("\r\n");
        sb.append("end").append("\r\n");
        sb.append("return results;");
        return sb.toString();
    }
}

Springboot集成Redis

Maven坐标引入

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

application-redis.yml配置文件

############################################################
# REDIS 配置
############################################################
spring:
  redis:
    database: 0
    host: 192.168.137.150
    port: 6379
    password:
    timeout: 6000  # 连接超时时间(毫秒)
    jedis:
      pool:
        max-active: 8 #连接池最大连接数(使用负值表示没有限制)
        max-wait: -1 #连接池最大阻塞等待时间(使用负值表示没有限制)
        max-idle: 10 #连接池中的最大空闲连接
        min-idle: 2 #连接池中的最小空闲连接

RedisTemplate配置

@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
	// 为了自己开发方便,一般直接使用 <String, Object>
	RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
	template.setConnectionFactory(factory);
	// Json序列化配置
	Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
	ObjectMapper om = new ObjectMapper();
	om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
	om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
	jackson2JsonRedisSerializer.setObjectMapper(om); // String 的序列化
	StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
	// key采用String的序列化方式
	template.setKeySerializer(stringRedisSerializer);
	// hash的key也采用String的序列化方式
	template.setHashKeySerializer(stringRedisSerializer);
	// value序列化方式采用jackson
	template.setValueSerializer(jackson2JsonRedisSerializer);
	// hash的value序列化方式采用jackson
	template.setHashValueSerializer(jackson2JsonRedisSerializer);

	template.afterPropertiesSet();
	return template;
}

分布式锁

/**
 * 获取分布式锁方法
 */
public Boolean getLock(String lockKey, String reqId, long time, TimeUnit unit) {
    if (Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(lockKey, reqId, time, unit))) {
        System.out.printf("%s 抢锁成功 ,lockKey:%s,reqId:%s,time:%s,unit:%s%n", ThreadSleepUtils.ThreadName(), lockKey, reqId, time, unit);
        return true;
    }
    return false;
}

/**
 * 释放锁的脚本
 */
DefaultRedisScript<Long> releaseLockScript = 
new DefaultRedisScript<>("if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end", Long.class);

/**
 * 释放分布式锁
 */
public void releaseLock(String lockKey, String reqId) {
    Optional.ofNullable(redisTemplate.execute(releaseLockScript, Collections.singletonList(lockKey), reqId)).ifPresent(flag -> {
        if (flag == 1L) {
            System.out.printf("%s 释放锁成功 lockKey:%s,reqId:%s %n", ThreadSleepUtils.ThreadName(), lockKey, reqId);
        } else {
            System.out.printf("%s 释放锁失败 lockKey:%s,reqId:%s %n", ThreadSleepUtils.ThreadName(), lockKey, reqId);
        }
    });
}

分布式限流

//编写 redis Lua 限流脚本
DefaultRedisScript<Number> limitScript = new DefaultRedisScript<>(buildLuaScript(), Number.class);
public String buildLuaScript() {
    StringBuilder lua = new StringBuilder();
    lua.append("local c c = redis.call('get',KEYS[1])");
    // 调用不超过最大值,则直接返回
    lua.append(" if c and tonumber(c) > tonumber(ARGV[1]) then");
    lua.append(" return tonumber(c)");
    lua.append(" end");
    // 执行计算器自加
    lua.append(" c = redis.call('incr',KEYS[1])");
    // 从第一次调用开始限流,设置对应键值的过期
    lua.append(" if tonumber(c) == 1 then redis.call('expire',KEYS[1],ARGV[2])");
    lua.append(" end");
    lua.append(" return c");
    return lua.toString();
}
//分布式限流示例
@Test
public void limitTest() {
    String limitKey = "bill_order";
    for (int i = 0; i < 10; i++) {
        executor.submit(() -> {
            if (isLimit(limitKey, 5, 1)) {
                return;
            }
            ThreadSleepUtils.log("进行业务操作处理逻辑");
            ThreadSleepUtils.sleepMS(RandomUtils.nextInt(100, 300));
        });
    }
    ThreadSleepUtils.sleep(500);
}
//判定是否触发限流
public boolean isLimit(String limitKey, Integer max, Integer second) {
    Number count = redisTemplate.execute(limitScript, Collections.singletonList(limitKey), max.toString(), second.toString());
    if (count.intValue() > max) {
        ThreadSleepUtils.log("触发限流 count:" + count.intValue() + ",limitKey:" + limitKey + ",max:" + max + ",second:" + second);
        return true;
    }
    return false;
}