Redis

2018-07-23  本文已影响0人  赵铁柱啊

Redis

什么是redis

Redis 是一个基于内存的高性能key-value数据库。

Reids的特点

Redis本质上是一个Key-Value类型的内存数据库,因为是纯内存操作,Redis的性能非常出色,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB。 Redis最大的魅力是支持保存多种数据结构,此外单个value的最大限制是1GB 。

使用redis有哪些好处?

(1) 速度快,因为数据存在内存中

(2) 支持多种数据结构,支持string,list,set,sorted set,hash

(3) 支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行

(4) 丰富的特性:可用于缓存,消息,按key设置过期时间,过期后将会自动删除

Redis是单进程单线程的

redis利用队列技术将并发访问变为串行访问,消除了传统数据库串行控制的开销

Redis的并发竞争问题如何解决?

Redis为单进程单线程模式,采用队列模式将并发访问变为串行访问。Redis本身没有锁的概念,Redis对于多个客户端连接并不存在竞争,但是在Jedis客户端对Redis进行并发访问时会发生连接超时、数据转换错误、阻塞、客户端关闭连接等问题,这些问题均是由于客户端连接混乱造成。对此有2种解决方法:

1.客户端角度,为保证每个客户端间正常有序与Redis进行通信,对连接进行池化,同时对客户端读写Redis操作采用内部锁synchronized。

2.服务器角度,利用setnx实现锁。

是否使用过Redis集群,集群的原理是什么?

Redis Sentinel['sɛntɪnl]着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。

Redis Cluster['klʌstɚ]着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。

Redis可以做什么(使用场景)

什么是分布式锁

分布式锁是相对线程锁和进程锁而言的。

名称 描述
线程锁 主要用来给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。
进程锁 为了控制同一操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访问其他进程的资源,因此无法通过synchronized等线程锁实现进程锁。
分布式锁 当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问。

分布式锁的使用场景

有这样一个情境,线程A和线程B都共享某个变量X。

单机情况下,线程之间共享内存,只需要使用线程锁就可以解决并发问题。

但是分布式情况下,线程A和线程B不再在同一个JVM中,线程锁不起作用,这个时候就必须使用分布式锁。

确保分布式锁可以用,需满足什么条件

  1. 互斥性。在任意时刻,只有一个客户端能持有锁。
  2. 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  3. 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
  4. 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

分布式锁的实现方式

1.引入jedis依赖

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>

2.获取锁的代码实现

public class RedisTool {
    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
}

可以看到,我们加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time),这个set()方法一共有五个形参:

总的来说,执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。

3.释放锁代码实现

public class RedisTool {
    private static final Long RELEASE_SUCCESS = 1L;
    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
}

可以看到,我们解锁只需要两行代码就搞定了!第一行代码,我们写了一个简单的Lua脚本代码,上一次见到这个编程语言还是在《黑客与画家》里,没想到这次居然用上了。第二行代码,我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。

那么这段Lua代码的功能是什么呢?其实很简单,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。那么为什么要使用Lua语言来实现呢?因为要确保上述操作是原子性的。

redis部署方式

部署方式 运作模式 分片 可用性
单实例模式 单台redis完成所有请求任务,宕机则无法工作 由一台实例独立完成 无法复用和不具备容错性,只能进行服务器物理扩展。
['sɛntɪnl]sentinel主从配置 由sentinel哨兵处理故障和完成故障转移,也是性能的瓶颈所在 每个主实例分担1/N的数据量 需要配置sentinel来实现复制和高可用性
集群模式 无中心节点,每个节点之间都有通信,当节点较多时资源消耗较多 按照槽来进行分片,通过为每个节点指派不同数量的槽,可以控制不同节点负责的数据量和请求数量。 集群的节点内置了复制和高可用性。

单实例模式 :单实例模式是指单台redis完成所有请求任务,因此复用和不具备容错性;同时在单台机器上如果只启用一个redis实例会造成资源浪费 。

Redis集群 :Redis 集群是一个由多个节点组成的分布式服务器群,它具有复制、高可用和分片特性。Redis集群不需要sentinel哨兵也能完成节点移除和故障转移的功能。需要将每个节点设置成集群模式,这种集群模式没有中心节点,多个节点之间存在着网络通信的消耗。

Redis Sentinel集群 :Sentinel是一个管理redis实例的工具,它可以实现对redis的监控、通知、自动故障转移。sentinel不断地检测redis实例是否可以正常工作,通过API向其他程序报告redis的状态,如果redis master不能工作,则会自动启动故障转移进程,将其中的一个slave提升为master,其他的slave重新设置新的master服务器。

Redis集群概念

  1. 分布式:Redis集群是一个由多个Redis服务器组成的分布式网络服务器群,集群中的各个服务器被称为节点(node),这些节点会相互连接并进行通信。分布式的Redis集群没有中心节点,所以用户不必担心某个节点会成为整个集群的性能瓶颈。

  2. 复制 :Redis 集群的每个节点都有两种角色可选,一个是主节点(master node),另一个是从节点(slavenode),其中主节点用于储存数据,而从节点则是某个主节点的复制品。当用户需要处理更多读请求的时候,可以添加从节点以扩展系统的读性能。因为Redis集群重用了单机Redis复制特性的代码,所以集群的复制行为和我们之前介绍的单机复制特性的行为是完全一样的。

  3. 节点故障检测和自动故障转移 :Redis 集群的主节点内置了类似Redis Sentinel的节点故障检测和自动故障转移功能,当集群中的某个主节点下线时,集群中的其他在线主节点会注意到这一点,并对已下线的主节点进行故障转移。集群进行故障转移的方法和Redis Sentinel进行故障转移的方法基本一样,不同的是,在集群里面,故障转移是由集群中其他在线的主节点负责进行的,所以集群不必另外使用Redis Sentinel 。

  4. 分片 :集群使用分片来扩展数据库的容量,并将命令请求的负载交给不同的节点来分担。
    集群将整个数据库分为 16384 个槽(slot),所有键都属于这 16384 个槽的其中一个,计算键 key属于哪个槽的公式为 slot_number = crc16(key) % 16384 ,其中 crc16 为 16 位的循环冗余校验和函数。集群中的每个主节点都可以处理 0 个至 16384 个槽,当 16384 个槽都有某个节点在负责处理时,集群进入上线状态,并开始处理客户端发送的数据命令请求。

  5. 转向 :对于一个被指派了槽的主节点来说,这个主节点只会处理属于指派给自己的槽的命令请求。如果一个节点接收到了与自己处理的槽无关的命令请求,那么节点会向客户端返回一个转向错误(redirection error),告诉客户端,哪个节点负责处理这条命令,之后客户端需要根据错误中包含的地址和端口号重新向正确的节点发送命令请求。

  6. Redis集群客户端 :因为集群功能比起单机功能要复杂得多,所以不同语言的 Redis 客户端通常需要为集群添加特别的支持,或者专门开发一个集群客户端。

    目前主要的 Redis 集群客户端(或者说,支持集群功能的 Redis 客户端)有以下这些:
    - redis-rb-cluster:antirez 使用 Ruby 编写的 Redis 集群客户端,集群客户端的官方实现。
    - predis:Redis 的 PHP 客户端,支持集群功能。
    - jedis:Redis 的 JAVA 客户端,支持集群功能。
    - StackExchange.Redis:Redis 的 C# 客户端,支持集群功能。
    -内置的 redis-cli :在启动时给定 -c 参数即可进入集群模式,支持部分集群功能。

    redis-cluster架构图

redis-cluster架构图
  1. 所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽。
  2. 节点的fail是通过集群中超过半数的节点检测失效时才生效。
  3. 客户端与redis节点直连,不需要中间proxy层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。
  4. redis-cluster把所有的物理节点映射到[0-16383]slot上,cluster 负责维护node<->slot<->value

Redis集群中内置了 16384 个哈希槽,当需要在 Redis 集群中放置一个 key-value 时,redis 先对key 使用 crc16 算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点

redis-cluster投票:容错

redis-cluster投票:容错

1.投票过程是集群中所有master参与,如果半数以上master节点与master节点通信超时(cluster-node-timeout),认为当前master节点挂掉.

2.什么时候整个集群不可用(cluster_state:fail)

(1)如果集群任意master挂掉,且当前master没有slave.集群进入fail状态,也可以理解成集群的slot映射[0-16383]不完整时进入fail状态 。

(2)如果集群超过半数以上master挂掉,无论是否有slave,集群进入fail状态。

客户端对Redis集群的使用方法

(1)使用redis命令行客户端连接

[root@localhost redis-cluster]# ./redis1/redis-cli -p 7001 -c
127.0.0.1:7001> get a
-> Redirected to slot [15495] located at 192.168.37.131:7003
(nil)
192.168.37.131:7003> 

一定要加-c参数,节点之间就可以互相跳转

(2)使用jedis连接

package com.pc.jedis.test;
import java.util.HashSet;
import java.util.Set;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
 
/**
 * Jedis集群测试
 * 
 * @author Switch
 * @data 2017年2月11日
 * @version V1.0
 */
public class JedisClusterTest {
    public static void main(String[] args) {
        // 创建并填充节点信息
        Set<HostAndPort> nodes = new HashSet<>();
        nodes.add(new HostAndPort("192.168.37.131", 7001));
        nodes.add(new HostAndPort("192.168.37.131", 7002));
        nodes.add(new HostAndPort("192.168.37.131", 7003));
        nodes.add(new HostAndPort("192.168.37.131", 7004));
        nodes.add(new HostAndPort("192.168.37.131", 7005));
        nodes.add(new HostAndPort("192.168.37.131", 7006));
 
        // 创建JedisCluster对象
        JedisCluster jedisCluster = new JedisCluster(nodes);
 
        // 使用jedisCluster操作redis
        String key = "jedisCluster";
        String setResult = jedisCluster.set(key, "hello redis!");
        System.out.println(setResult);
 
        String getResult = jedisCluster.get(key);
        System.out.println(getResult);
 
        // 关闭jedisCluster(程序执行完后才能关闭,内部封装了连接池)
        jedisCluster.close();
    }
}

Redis Cluster['klʌstɚ] 集群

Redis集群搭建的方式有多种,例如使用zookeeper等,但从redis 3.0之后版本支持redis-cluster集群,Redis-Cluster采用无中心结构,每个节点保存数据和整个集群状态,每个节点都和其他所有节点连接。

结构特点:

1、所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽。
2、节点的fail是通过集群中超过半数的节点检测失效时才生效。
3、客户端与redis节点直连,不需要中间proxy层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。
4、redis-cluster把所有的物理节点映射到[0-16383]slot上(不一定是平均分配),cluster 负责维护node<->slot<->value。

​ 5、Redis集群预分好16384个桶,当需要在 Redis 集群中放置一个 key-value 时,根据 CRC16(key) mod 16384的值,决定将一个key放到哪个桶中。

上一篇下一篇

猜你喜欢

热点阅读