2024-09-03

解释:

Redis Cluster 是 Redis 的分布式版本,其数据被分布在不同的节点上。为了保证数据均匀分布在不同的节点上,Redis Cluster 采用了哈希槽(hash slot)的概念,其中每个节点负责维护一定数量的哈希槽。当你尝试对不属于同一个哈希槽的键执行操作时(例如:使用 MGET, MSET 等操作跨多个键时),Redis 会返回 CROSSSLOT Keys in request don‘t hash to the same slot 错误。

解决方法:

  1. 使用哈希标签:将相关的键通过使用 {key}:{tag} 的方式来确保它们落在同一个哈希槽中。
  2. 对键进行哈希计算并重新分配:如果键必须分布在不同的节点上,那么需要手动将这些键分配到正确的节点上。
  3. 使用单个键操作:对于需要执行的操作,尽量使用单个键,而不是多个键,以避免跨槽问题。
  4. 使用 Redis 的集群命令:Redis 提供了一些集群命令,如 {},可以用来处理多键操作,这些命令会自动处理跨槽问题。

具体使用哪种方法取决于你的应用需求和数据分布需求。

2024-09-03

在Java中,你可以使用Jedis或Lettuce库来操作Redis。以下是使用Jedis连接Redis并执行一些基本命令的示例代码:




import redis.clients.jedis.Jedis;
 
public class RedisExample {
    public static void main(String[] args) {
        // 连接本地的 Redis 服务
        Jedis jedis = new Jedis("localhost");
        System.out.println("连接成功");
        // 设置 redis 字符串数据
        jedis.set("myKey", "myValue");
        // 获取存储的数据并输出
        System.out.println("redis 存储的字符串为: " + jedis.get("myKey"));
        
        // 操作 Redis 列表
        jedis.lpush("myList", "element1");
        jedis.lpush("myList", "element2");
        // 获取列表数据并输出
        System.out.println("redis 列表数据为: " + jedis.lrange("myList", 0, -1));
        
        // 关闭连接
        jedis.close();
    }
}

确保在运行此代码之前,你的机器上已经安装了Redis服务器,并且Jedis库已经添加到项目的依赖中。如果你使用的是Maven,可以在pom.xml中添加如下依赖:




<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>最新版本号</version>
</dependency>

以上代码展示了如何使用Jedis连接Redis,并执行基本的命令,如SET、GET、LPUSH等。在实际应用中,你可能需要处理连接池、异常处理等更复杂的情况,但这是操作Redis的基本模式。

2024-09-03



import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
 
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
 
public class RedisClusterExample {
    public static void main(String[] args) throws IOException {
        // 假设你已经配置好了Redis Cluster,并且知道其中一些节点的地址
        Set<HostAndPort> jedisClusterNodes = new HashSet<>();
        jedisClusterNodes.add(new HostAndPort("127.0.0.1", 7000));
        jedisClusterNodes.add(new HostAndPort("127.0.0.1", 7001));
        // ... 添加更多节点
 
        // 创建JedisCluster对象
        JedisCluster jedisCluster = new JedisCluster(jedisClusterNodes);
 
        // 使用JedisCluster进行操作
        jedisCluster.set("key", "value");
        String value = jedisCluster.get("key");
        System.out.println("获取的值为: " + value);
 
        // 关闭JedisCluster对象
        jedisCluster.close();
    }
}

这段代码展示了如何使用Jedis客户端库来连接Redis Cluster,并执行一个简单的设置和获取操作。在实际应用中,你需要提供正确的Redis Cluster节点地址和端口。这个例子假设你已经有一个运行中的Redis Cluster,并且知道其中一些节点的地址。

2024-09-03



#include "fmacros.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
 
#include "hiredis/hiredis.h"
 
void printSlotsInfo(redisClusterContext *cc) {
    redisClusterNode *node;
    int i;
 
    for (i = 0; i < cc->cluster->size; i++) {
        node = cc->cluster->nodes[i];
        printf("Node %d: ", node->id);
        if (node->fail_state != REDIS_NODE_FAIL_STATE_NONE) {
            printf("has fail state: %d\n", node->fail_state);
        } else {
            printf("is ok\n");
        }
    }
}
 
int main() {
    redisClusterContext *cc = redisClusterConnect("127.0.0.1", 7000);
    redisReply *reply;
    if (cc == NULL) {
        printf("Error in connect\n");
        exit(-1);
    }
 
    // 关闭自动重定向
    cc->flags &= ~(REDIS_CLUSTER_FLAG_NO_REDIRECTION);
 
    // 模拟故障
    system("kill -9 `ps aux | grep redis-server | grep 7000 | awk '{print $2}'`");
    sleep(5); // 等待Redis节点重启
 
    printf("After failure:\n");
    printSlotsInfo(cc);
 
    // 再次尝试执行命令
    reply = redisClusterCommand(cc, "GET foo");
    if (reply == NULL) {
        printf("Execution error\n");
    } else {
        printf("Reply: %s\n", reply->str);
        freeReplyObject(reply);
    }
 
    // 清理
    redisClusterFree(cc);
    return 0;
}

这段代码首先尝试连接到Redis集群,然后关闭自动重定向功能,模拟一个节点宕机,并在宕机后打印出集群的状态信息。最后,再次尝试执行一个命令,并输出结果。这个过程演示了Redis集群在出现故障时的自动故障转移机制。

2024-09-03

Redis 主从同步和哨兵机制是用来保证Redis服务的高可用性和数据备份的。

  1. Redis主从同步

主从同步是指将一个Redis服务器的数据同步到另一个服务器,前者称为主节点(master),后者称为从节点(slave)。

配置主从同步的步骤如下:

  • 在从节点的配置文件中加入 slaveof <master-ip> <master-port> 来指定主节点的IP和端口。
  • 在从节点执行 SLAVEOF <master-ip> <master-port> 命令也可以指定主节点。

例如:




slaveof 192.168.1.100 6379
  1. Redis哨兵机制

当主节点出现故障时,哨兵(sentinel)会检测到并自动将一个从节点提升为新的主节点,继续提供服务。

哨兵机制的配置步骤如下:

  • 在哨兵的配置文件中指定要监控的主节点和其地址。
  • 启动哨兵,使用 redis-sentinel /path/to/your/sentinel.conf 命令。

例如,sentinel.conf 配置文件中可能包含如下内容:




sentinel monitor mymaster 192.168.1.100 6379 2
sentinel down-after-milliseconds mymaster 30000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 180000

mymaster 是主节点的名字,后面是主节点的IP和端口,以及最少需要多少个哨兵同意才进行故障转移。

2024-09-03

JWT(JSON Web Token)是一种用于双方之间传递安全信息的简洁的、URL安全的表示方法。

以下是使用JWT的一般流程:

  1. 用户登录:用户使用用户名和密码登录。
  2. 验证凭据:服务器验证用户名和密码。
  3. 创建JWT:服务器验证成功后,创建一个JWT。
  4. 返回JWT:服务器返回JWT给客户端。
  5. 客户端存储JWT:客户端通常将JWT存储在localStorage或sessionStorage中。
  6. 后续请求:客户端在后续请求中将JWT添加到请求头中。
  7. 服务器验证JWT:服务器检查请求头中的JWT,如果JWT有效,则提供访问权限。

JWT结构通常如下:




Header.Payload.Signature

Header通常指定签名算法:




{
  "alg": "HS256",
  "typ": "JWT"
}

Payload包含实际的声明:




{
  "iss": "Issuer",
  "sub": "Subject",
  "aud": "Audience",
  "exp": "Expiration Time",
  "nbf": "Not Before",
  "iat": "Issued At",
  "jti": "JWT ID"
}

Signature通过以下方式创建:




HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

以下是一个简单的Java JWT工具类示例:




import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.Claims;
 
public class JwtUtil {
    private static final String SECRET_KEY = "verySecret"; // 应该是一个复杂的密钥
 
    public static String generateToken(User user) {
        return Jwts.builder()
                .setSubject(user.getUsername())
                .setExpiration(new Date(System.currentTimeMillis() + 864000000)) // 有效期10天
                .signWith(SignatureAlgorithm.HS512, SECRET_KEY)
                .compact();
    }
 
    public static boolean validateToken(String token, UserDetails userDetails) {
        String username = Jwts.parser()
                .setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
 
        return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
    }
 
    private static boolean isTokenExpired(String token) {
        Date expiration = Jwts.parser()
                .setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody()
                .getExpiration();
 
        return expiration.before(new Date());
    }
}

在与Redis结合使用时,可以在用户登录时创建JWT并将其存储在Redis中,并在后续请求中验证Redis中的JWT是否有效。




public String login(User user) {
    String token = JwtUtil.generateToken(user);
    redisTemplate.opsForValue().set(token, user.getUsername(), 10, TimeUnit.DAYS);
    return token;
}
 
public boolean validateToken(String token) {
    boolean isTokenValid = false;
    String username = (String) redisTemplate.opsForValue().get(token);
    if (username != null && !username.isEmpty()) {
        isTokenValid = true;
2024-09-03



import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import java.time.LocalDateTime;
 
// 使用Jackson的注解来自定义LocalDateTime的序列化和反序列化
public class CustomLocalDateTimeMixin {
 
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    private LocalDateTime dateTime;
 
    public LocalDateTime getDateTime() {
        return dateTime;
    }
 
    public void setDateTime(LocalDateTime dateTime) {
        this.dateTime = dateTime;
    }
}

这段代码定义了一个混入类(Mixin),用于自定义LocalDateTime的序列化和反序列化。在序列化时使用LocalDateTimeSerializer,在反序列化时使用LocalDateTimeDeserializer。这样,在Spring Boot应用中使用Redis作为缓存数据库时,就可以正确地存储和读取LocalDateTime类型的数据。

2024-09-03

问题描述的是Redis缓存问题的一些典型情况,包括缓存穿透、击穿、雪崩、双写一致性、持久化、数据淘汰策略和数据删除。以下是对这些问题的简要解释和解决方案:

  1. 缓存穿透:

    问题:查询不存在的数据,缓存和数据库都没有,导致每次请求都会打到数据库上。

    解决方案:使用布隆过滤器或者缓存空对象。

  2. 缓存击穿:

    问题:一个Key同时大量并发请求,该Key正好过期,导致大量请求打到数据库。

    解决方案:设置热点Key的过期时间随机化,或使用分布式锁。

  3. 雪崩:

    问题:大量Key同时过期,导致数据库压力剧增。

    解决方案:不同的Key设置不同的过期时间,或者使用相同的过期时间并加上随机值,避免大量Key在同一时间过期。

  4. 双写一致性:

    问题:在数据更新时,数据库和缓存更新成功,但是因为某种原因(如网络问题)导致更新不一致。

    解决方案:使用事务或锁保证操作的原子性,或使用消息队列异步通知更新缓存。

  5. 持久化:

    问题:数据丢失。

    解决方案:使用RDB或AOF进行数据持久化。

  6. 数据淘汰策略:

    问题:内存不足,需要淘汰数据。

    解决方案:使用LRU、LFU等淘汰策略。

  7. 数据删除:

    问题:数据删除后如何处理。

    解决方案:通过删除指令删除缓存数据,并确保数据库数据也被删除。

以上是对这些问题的简要解释和解决方案,具体实施时需要根据实际情况进行调整。

2024-09-03



#include <sys/epoll.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
 
int main() {
    // 创建一个epoll的文件描述符
    int epfd = epoll_create(1000);      
 
    // 设置非阻塞模式
    int flags = fcntl(epfd, F_GETFL);
    fcntl(epfd, F_SETFL, flags | O_NONBLOCK);
 
    // 定义epoll_event结构体变量,用于后续的添加和处理事件
    struct epoll_event event;
    event.events = EPOLLIN; // 注册读事件
    event.data.fd = STDIN_FILENO; // 标准输入文件描述符
 
    // 将文件描述符添加到epoll中
    epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &event);
 
    // 创建一个epoll_event数组,用于存储就绪的事件
    struct epoll_event ready_events[10];
    int nready;
 
    // 循环等待事件
    while (1) {
        // 等待直到有事件发生
        nready = epoll_wait(epfd, ready_events, 10, -1);
 
        // 处理所有就绪的事件
        for (int i = 0; i < nready; i++) {
            if (ready_events[i].data.fd == STDIN_FILENO) {
                // 如果是标准输入可读,则读取并处理输入
                char buf[1024];
                int count = read(STDIN_FILENO, buf, 1023);
                if (count > 0) {
                    buf[count] = '\0'; // 确保字符串以null结尾
                    printf("Read: %s", buf);
                }
            }
        }
    }
 
    // 关闭epoll文件描述符
    close(epfd);
    return 0;
}

这段代码展示了如何使用epoll来处理标准输入的可读事件。它首先创建一个epoll的文件描述符,然后将标准输入的文件描述符添加到epoll中,并注册读事件。接着,它在一个循环中调用epoll\_wait来等待事件,并处理任何到来的输入。这个例子简单而直接地展示了epoll的使用方法,对于理解Redis的高性能设计有重要的教育意义。

2024-09-03

以下是使用docker-compose搭建一主两重两哨兵的Redis哨兵模式的示例代码:

首先,创建一个名为 docker-compose.yml 的文件,内容如下:




version: '3'
 
services:
  redis-master:
    image: redis:6.0.9
    ports:
      - "6379:6379"
    command: redis-server --appendonly yes --port 6379
 
  redis-slave-1:
    image: redis:6.0.9
    ports:
      - "6380:6379"
    command: redis-server --slaveof redis-master 6379 --appendonly yes --port 6380
 
  redis-slave-2:
    image: redis:6.0.9
    ports:
      - "6381:6379"
    command: redis-server --slaveof redis-master 6379 --appendonly yes --port 6381
 
  redis-sentinel-1:
    image: redis:6.0.9
    ports:
      - "26379:26379"
    command: redis-sentinel /etc/redis/sentinel.conf
    volumes:
      - ./sentinel.conf:/etc/redis/sentinel.conf
 
  redis-sentinel-2:
    image: redis:6.0.9
    ports:
      - "26380:26379"
    command: redis-sentinel /etc/redis/sentinel.conf
    volumes:
      - ./sentinel.conf:/etc/redis/sentinel.conf
 

然后,在同一目录下创建哨兵配置文件 sentinel.conf,内容如下:




port 26379
dir /tmp
sentinel monitor mymaster redis-master 6379 2
sentinel down-after-milliseconds mymaster 30000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 180000

请确保修改 sentinel.conf 文件的权限,使其可以被 Docker 容器读取。

最后,通过以下命令启动服务:




docker-compose up -d

这将会启动一个Redis主实例、两个Redis从实例(重复配置)以及两个Redis哨兵实例。哨兵实例共享一个配置文件,通过卷(volume)挂载到容器内部。

注意:

  1. 确保你的 docker-compose.yml 文件和 sentinel.conf 文件的路径正确,并且 sentinel.conf 文件权限允许。
  2. 版本号(如 6.0.9)需要根据你的实际Redis镜像版本进行替换。