Spring Boot之Redis

Redis相关介绍

Redis

       Redis是一个开放源代码(BSD许可)的内存中数据结构存储,用作数据库,缓存和消息代理。它支持数据结构,例如字符串,哈希,列表,集合,带范围查询的排序集合,位图,超日志,带有半径查询和流的地理空间索引。Redis具有内置的复制,Lua脚本,LRU逐出,事务和不同级别的磁盘持久性,并通过Redis Sentinel和Redis Cluster自动分区提供了高可用性。

简单小结: redis是非关系型数据库(NoSQL),遵循了BSD开源协议,使用C语言编写(gcc)的key-value数据库。

Redis分别有两个版本,分别是Linux版和Windows版,入门建议从windows版开始使用,可视化工具叫做Redis Desktop Manager。

两个版本的区别:

Linux:性能高,商业项目基本都是使用该版本,需要安装c语言的环境。

Windows:性能虽然比Linux低一些,但是上手会比较容易,无需安装(解压后即可使用)。

redis官网: https://redis.io/

可视图化地址: https://redisdesktop.com/

优点:

  1. 支持多种数据类型

  2. 持久化存储

  3. 性能很好(全内存操作)

  4. 支持事务

BSD协议

       BSD开源协议是一个给于使用者很大自由的协议。可以自由的使用,修改源代码,也可以将修改后的代码作为开源或者专有软件再发布。当你发布使用了BSD协议的代码,或者以BSD协议代码为基础做二次开发自己的产品时,需要满足三个条件

  1. 如果再发布的产品中包含源代码,则在源代码中必须带有原来代码中的BSD协议。

  2. 如果再发布的只是二进制类库/软件,则需要在类库/软件的文档和版权声明中包含原来代码中的BSD协议。

  3. 不可以用开源代码的作者/机构名字和原来产品的名字做市场推广。

TCP/IP协议

       TCP/IP(传输控制协议/网际协议,俗称网络传输协议)是指能够在多个不同网络间实现信息传输的协议簇。TCP/IP协议不仅仅指的是TCP 和IP两个协议,而是指一个由 FTP、SMTP、TCP、UDP、IP等协议构成的协议簇, 只是因为在TCP/IP协议中TCP协议和IP协议最具代表性,所以被称为TCP/IP协议。

以下表格顺序为从上至下(物理层为最底层):

OSI七层模型TCP/IP四层模型功能TCP/IP协议簇
应用层应用层电子邮件,文件传输,虚拟终端等操作HTTP协议,FTP协议,SMTP协议等
表示层属于应用层数据加密,代码转换,数据格式化等操作无协议
会话层属于应用层建立或者解除与别的接点的联系无协议
传输层传输层提供端对端的接口TCP协议和UDP协议
网络层网络层为数据包去选择路由IP协议,ICMP协议等
数据链路层数据链路层传输地址的帧以及错误检测功能ARP协议,RARP协议等
物理层(硬件方面)属于数据链路层以二进制数据形式在物理媒体上传输数据ISO等

快速上手

了解什么是redis之后在进行一些对应的配置即可开始使用redis啦。

打开Redis服务

方法一:

  1. 使用Win+R快捷打开cmd页面

  2. 使用命令进入到存放Redis目录的位置

    如果不是放在C盘则先进入存放Redis的系统盘中:C:\Users>e:

    然后使用 “cd 文件夹名” 的方式进入对应文件夹中:E:\ >cd Redis

  3. 进入之后输入redis-server.exe即可打开redis服务

  4. 最后在打开一个cmd页面,找到Redis目录后输入redis-cli 即可开始使用

    注: 第一个打开的cmd页面不要关闭,否则无法使用!

方法二:

  1. 使用Win+R快捷打开cmd页面

  2. 使用命令进入到存放Redis目录的位置

  3. 然后输入以下命令即可安装Redis服务

    1. 安装redis服务
    redis-server.exe --service-install redis.windows.conf
    
    2. 卸载redis服务
    redis-server --service-uninstall
    

    注: 需要在电脑中把安装的Redis服务设置为自动启动!

  4. 下载Redis可视图化工具并安装打开,输入对应IP地址与端口号连接Redis即可开始使用,Redis默认是没有密码的。

在这里插入图片描述

使用简单命令进行操作: 新增属性、查看属性和删除属性

1.新增一个名为name的属性
set name laoyang
    
2.查看name属性的值
get name
    
3.删除一个属性名为name的值
del name

Redis客户端的使用(Jedis)

导入依赖

<!-- jedis依赖 -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.1.0</version>
</dependency>

导入依赖后就可以直接使用实体类进行测试了

@SpringBootTest
@RunWith(SpringRunner.class)
public class StudentTests {
    //创建简单的属性与查看
    @Test
    public void testJedis(){
        /**
         * 创建jedis对象,配置IP地址与端口号即可连接Redis
         * 如果使用的是自己的端口,则需要先启动自己的服务器
         */
        Jedis jedis = new Jedis("127.0.0.1",6379);

        //使用set命令创建数据
        jedis.set("test","ceshi");

        //使用get命令在控制台打印输出刚才创建的数据
        //因为创建的值是中文,所以在cmd页面使用get查询时会转换为乱码
        System.err.println("test="+jedis.get("test"));

        //释放资源
        jedis.close();
    }
}

Socket的使用

java跟redis进行通信,一定需要遵循TCP/IP协议,java中实现TOP/IP协议:Socket

介绍:

       套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合。

使用scoket连接redis

//使用scoket连接redis
public class RedisSocket {
    //Jedis和Scoket连接redis时都需要知道IP地址和端口信息
    //IP地址
    private String host;
    //端口号
    private Integer port;
    //Socket(默认为空)
    private Socket socket;
    
    //建立通信前需要经过IO流
    //输入流
    private InputStream inputStream;
    //输出流
    private OutputStream outputStream;

    //初始化IP地址与端口号
    public RedisSocket(String host, Integer port) {
        this.host = host;
        this.port = port;
    }

    //有了IP地址和端口号后就可以开始建立连接了
    public boolean isConnection(){
        //判断是否有连接,如果有则直接连接,没有则创建连接
        /**
         * 必须要判断:
         * !socket.isClosed():判断连接是否关闭
         * socket.isConnected():判断是否已经连接
         * socket.isBound():判断是否绑定
         */
        if (socket != null && !socket.isClosed() && socket.isConnected() && socket.isBound()){
            return true;
        }

        //如果没有连接的话则创建连接
        try {
            /**
             * sockey一旦创建成功,就意味着三次握手完成。
             * 三次握手:TOC建立连接时需要传输三个数据包,俗称三次握手。
             * 三次握手完成后就表示已经连接成功了!
             */
            socket = new Socket(host, port);
            
            //使用IO流
            inputStream = socket.getInputStream();
            outputStream = socket.getOutputStream();
        } catch (IOException e) {
            e.printStackTrace();
            //如果构建失败则返回false
            return false;
        }
        return true;
    }

    //连接成功后即可发送指令,网络传输的数据类型为byte数组
    public String sendCommand(byte[] bytes){
        //发送指令之前需要先判断是否已经连接
        if (isConnection()){
            System.err.println("连接成功!");
            try {
                //使用输出流发送请求到服务器
                outputStream.write(bytes);
                System.err.println("指令发送成功!");

                //处理结果
                int length = 0; //初始为0
                byte[] result = new byte[1024]; //每次读取1024个字节,可以随便写
                System.err.println("读取相应的结果:");

                while ((length = inputStream.read(result))>0){
                    /**
                     * 因为返回的是String类型,所以需要转换一下
                     * 三个值分别表示每次读多少,从哪里开始读,到哪里结束
                     */
                    System.err.println("成功拿到结果!");
                    return new String(result,0,length);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        //没有建立连接则返回null
        return null;
    }
}

编写测试类进行测试

@SpringBootTest
@RunWith(SpringRunner.class)
public class StudentTests {
    public static void main(String[] args) {
        //创建连接
        RedisSocket redisSocket = new RedisSocket("127.0.0.1",6379);
        //发送指令
        String getCommand = "get name";
        redisSocket.sendCommand(getCommand.getBytes());
    }
}

运行后会发现执行到 RedisSocket 类中的 while 循环时就会变成阻塞状态(BIO阻塞)

原因: 因为发送的指令没有遵循 Redis 的协议进行,所以 Redis 无法解析对应指令!

       Redis客户端使用称为RESP(REdis序列化协议)的协议与Redis服务器进行通信。虽然该协议是专为Redis设计的,但它可以用于其他客户端-服务器软件项目。

RESP协议格式:

get name
C: *2\r\n		*表示数组,长度为2
C: $3\r\n		$表示字符串,长度为3
C: GET\r\n		GET表示具体的指令,长度就是3
C: $4\r\n		$表示字符串,长度为4
C: name\r\n		name表示具体的指令,长度就是4

set name laoyang
C: *3\r\n
C: $3\r\n
C: SET\r\n
C: $4\r\n
C: name\r\n
C: $7\r\n
C: laoyang\r\n

Jedis也就是这个协议!

所以需要验证 Jedis 是否遵循了 Redis 协议

分别有两种方式

  1. 抓包工具(网上有很多,这里就不细说了)

    抓包:
           抓包就是将网络传输发送与接收的数据包进行截获、重发、编辑、转存等操作,也用来检查网络安全。抓包也经常被用来进行数据截取等。

  2. 简易的服务端

    开启一个服务端

    /**
     * 开启一个服务端,监听8080端口
     * 接收发到8080的请求
     */
    public class CapturePackage {
    
        public static void main(String[] args) {
            CaptureThread captureThread = new CaptureThread();
            captureThread.run();
        }
    
        static class CaptureThread extends Thread{
            @Override
            public void run() {
                //建立ServerSocket服务(ServerSocket是服务端的Socket)
                ServerSocket serverSocket = null;
                Socket socket = null;
                //需要用到输入流
                DataInputStream inputStream = null;
    
                //构建服务器
                try {
                    //启动服务,监听指定端口(此处为8080)
                    serverSocket = new ServerSocket(8080);
                    //等待连接
                    socket = serverSocket.accept();
    
                    //还可以设置一些状态,比如每过一段时间就询问你是否已经连接成功等...根据情况来设置
                    //socket.setKeepAlive(true);
    
                    //连接成功后进行处理
                    while (true){   //此处的死循环表示只要连接上了就一直可以连接
                        inputStream = new DataInputStream(socket.getInputStream());
                        //创建字节数组
                        byte[] buffer = {};
                        //获取实际可读的字节数
                        int bufferlength = inputStream.available();
                        int size = 0;
    
                        //在可读字节数不等于 0 时才会开始读取
                        while (bufferlength != 0){
                            //初始化字节数组,长度为可读字节数
                            buffer = new byte[bufferlength];
                            //开始读取
                            size += inputStream.read(buffer);
                            //读取完后再次获取可读字节数
                            bufferlength = inputStream.available();
                        }
    
                        //在有结果的情况下对结果进行打印
                        if (buffer.length != 0){
                            //把存放结果的字节数组转化成字符串(编码格式默认为 GB2312)
                            String str = new String(buffer,"GB2312");
                            System.err.println("接收的数据包如下:");
                            System.err.println(str);
                        }
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

    查看服务端获取的结果

    注意: 先运行服务端的 main 方法然后在运行测试类方法,然后运行完之后客户端会出现报错,那是正常的,因为我们只需要查看服务端获取的结果即可!

    //在测试类中执行以下方法
    @Test
    public void testJedis(){
        Jedis jedis = new Jedis("127.0.0.1",8080);
        System.err.println(jedis.get("name"));
    	jedis.close();
    }
    
    //执行完后在服务端看到的结果为
    接收的数据包如下:
    *2
    $3
    GET
    $4
    name
    

    编写协议类

    /**
     * Redis通信协议
     */
    public class RedisProtocol {
        //使用枚举设定提交方式(get/set),枚举在少量值时使用较好,在大数据量的情况下不建议使用
        public enum Command{
            GET,SET
        }
    
        //创建拼接格式使用的符号
        private static final String XIN = "*";
        private static final String MEI = "$";
        private static final String GAN = "\r\n";
    
        //处理提交的命令,处理成 Redis 协议的样子
        public static byte[] buildCommand(Command command,byte[]...bytes){  //提交方式,动态变化的值
            // 使用StringBuilder拼接字符串
            StringBuilder stringBuilder = new StringBuilder();
    
            // *2\r\n:*号开头,长度为参数的数据+1(因为指令也会占一位),最后使用\r\n换行
            stringBuilder.append(XIN).append(bytes.length+1).append(GAN);
            // $3\r\n:$号开头,长度为指令的长度,最后\r\n换行
            stringBuilder.append(MEI).append(command.name().length()).append(GAN);
            // GET\r\n:以指令名开头,最后\r\n换行
            stringBuilder.append(command.name()).append(GAN);
            // 以上三行为固定格式,接下来的部分为动态变化的格式,所以需要使用循环执行其它的参数
            for (byte[] arg:bytes){
                // $7\r\n:$开头,长度为参数长度,最后\r\n换行
                stringBuilder.append(MEI).append(arg.length).append(GAN);
                // laoyang\r\n:以参数开头(因为是字符串格式所以需要转换),最后\r\n换行
                stringBuilder.append(new String(arg)).append(GAN);
            }
            //因为返回类型为byte[],所以需要进行转换
            return stringBuilder.toString().getBytes();
        }
    }
    

    模拟 Redis 客户端

    /**
     * 模拟Redis客户端,类似于Jedis
     */
    public class RedisClient {
        private RedisSocket redisSocket;
    
        //初始化客户端
        public RedisClient(String host,Integer port){
            redisSocket = new RedisSocket(host,port);
        }
    
        //这里演示简单的操作,所以直接固定死就只有get和set指令
        //get指令
        public String Get(String key){
            //把指令通过RedisProtocol处理成符合RESP协议的字节数组,再把这个字节数组发送到Redis服务器
            String result = redisSocket.sendCommand(RedisProtocol.buildCommand(RedisProtocol.Command.GET,key.getBytes()));
            return result;
        }
    
        //set指令
        public String Set(String key,String value){
            String result = redisSocket.sendCommand(RedisProtocol.buildCommand(RedisProtocol.Command.SET,key.getBytes(),value.getBytes()));
            return result;
        }
    }
    

    测试

    @Test
    public void testRedis(){
        RedisClient client = new RedisClient("127.0.0.1",6379);
        client.Set("name","laoyang");
        System.err.println(client.Get("name"));
    }
    
    //因为使用的是自己的端口,所以可以直接运行该测试方法,运行结果如下
    连接成功!
    指令发送成功!
    读取相应的结果:
    成功拿到结果!
    连接成功!
    指令发送成功!
    读取相应的结果:
    成功拿到结果!
    $7
    laoyang
    //如果觉得输出的太多了,把在RedisSocket类中的输出删除或者注释即可
    

    通过以上案例,自己编写的客户端已经成功连接 Redis 服务器了,并且可以正常的进行 set 和 get 通信!但是还是会有一点问题,比如在多线程的情况下会出现线程安全的问题(线程不安全)。

    模拟多用户

    /**
     * 模拟 Redis 多用户操作
     */
    public class RedisThread implements Runnable {
        //模拟多用户set
        private RedisClient client; //客户端,一个用户一个客户端
        private String value;       //set指令的值
    
        //初始化用户
        public RedisThread(RedisClient client,String value){
            this.client = client;
            this.value = value;
        }
    
        public void run() {
            //发送指令
            client.Set("name",value);
        }
    }
    

    测试

    public class ThreadTests {
        public static void main(String[] args) {
            RedisClient redisClient = new RedisClient("127.0.0.1",6379);
    
            //创建线程池
            ExecutorService executorService = Executors.newCachedThreadPool();
            //模拟30个用户
            for (int i=0;i<30;i++){
                executorService.execute(new RedisThread(redisClient,"laoyang"+i));
            }
        }
    }
    

    测试后就可以发现本来每个用户只发送一个请求,但是Redis服务器却响应了多个结果,因为用户发送的byte数组在Socket中还没有来得及刷新(也就是没有发送到Redis服务器),别的用户线程的byte数组有到达了Socket,而且Socket中是流,没有办法知道当前流中有多少个字节数组,所以就会把多个字节数组,所以就会把多个字节数组同时发送到Redis服务器,所以就出现了多个响应的结果!

    改造成线程安全的

    可以使用连接池的方式解决线程安全的问题

    编写连接池类

    /**
     * Redis连接池类
     * 好处:
     * 1.减少创建连接的开销
     * 2.可以解决线程安全的问题
     */
    public class RedisPool {
        private LinkedBlockingQueue<RedisClient> pool;
        //统计借出连接的数量
        private int number = 0;
    
        //初始化连接池
        public RedisPool(String host,Integer port,int initSize){
            //创建了大小为initSize的连接池
            pool = new LinkedBlockingQueue<RedisClient>(initSize);
            //初始化连接
            for (int i= 0;i<initSize;i++){
                //每次都是新的客户端
                RedisClient client = new RedisClient("127.0.0.1",6379);
                try {
                    pool.put(client);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        //获得连接
        public RedisClient getRedisClient(){
            RedisClient client = null;
            // 满足条件表示还有连接
            if (number < pool.size()){
                try {
                    client = pool.take();
                    number++;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return client;
        }
    
        //归还连接
        public void closeRedisClient(RedisClient client){
            try {
                pool.put(client);
                number--;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    

    使用连接池后在模拟多用户的类中就不是使用单个客户端了,而是使用连接池

    /**
     * 模拟 Redis 多用户操作
     */
    public class RedisThread implements Runnable {
        //模拟多用户set
        private RedisPool pool; 	//替换成连接池
        private String value;       //set指令的值
    
        //初始化用户
        public RedisThread(RedisPool pool,String value){
            this.pool = pool;
            this.value = value;
        }
    
        public void run() {
            //去连接池中拿客户端
            RedisClient client = pool.getRedisClient();
            //发送指令
            client.Set("name",value);
            //执行完后归还连接
            pool.closeRedisClient(client);
        }
    }
    

    测试

    public class JedisTests {
    	public static void main(String[] args) {
            //初始化连接池(IP地址,端口号,连接池的大小)
            RedisPool pool = new RedisPool("127.0.0.1",6379,10);
            
            //创建线程池
            ExecutorService executorService = Executors.newCachedThreadPool();
            //模拟30个用户
            for (int i=0;i<30;i++){
                executorService.execute(new RedisThread(pool,"laoyang"+i));
            }
        }
    }
    

    使用连接池的方式则不会出现用户发送一次请求时有多个结果的情况,可以很好的保证线程安全!!!

    注: 运行后控制台报错是正常的,可以不用理会!

    Redis命令与特性

    字符串命令

    1. 设置键值对
    set key value
    
    2. 查看key
    get key
    
    3. 批量设置键值对
    mset key1 value1 key2 value2...
    
    4. 批量查看key的值
    mget key1 key2...
    
    5. 如果key的值为数字,则自增1,如果这个key不为数字,则新建一个值,默认为0,自增后变为1
    incr key
    
    6. 如果key的值为数字,则自减1,如果这个key不为数字,则新建一个值,默认为0,自增后变为-1
    decr key
    
    7. 将value追加到key的值中,如果这个key不存在,则会创建当前key,值为value
    append key value
    
    8. 查看指定key的值长度
    strlen key
    
    9. 从index(下标,从0开始)开始替换value的值
    setrange key index value
    
    10. 从start(0)开始截取字符串到end结束,如果是正数,表示从左到右截取。如果是负数,表示从右到左截取,右边第一位是-1。如果要截取整个字符串就是从0到-1!
    get range key start end
    
    11. 如果key的值为数字,则自增指定大小,如果这个key不为数字,则新建一个值,默认为0,自增后变为指定大小
    incrby key increment
    
    12. 如果key的值为数字,则自减指定大小,如果这个key不为数字,则新建一个值,默认为0,自减后变为负指定大小
    decrby key decrment
    
    13. 如果key值为小数,则自增指定大小(当小数为0时自动转为整数),如果没有该key,则创建一个值,值为自增后的值
    incrbyfloat key increment
    
    14. 删除指定的key
    del key
    
    15. 在key不存在时设置一个值,如果当前有key值,则无法设置
    setnx key value
    
    16. 创建一个临时的值,为它设置过期的时间(milliseconds)
    psetex key milliseconds value
    
    ...
    

    HASH命令

    hash命令都是以 h 开头的

    1. 修改hash表key中的字段field为value
    hset key field value
    
    2. 获取存储在哈希表中指定字段的值
    hget key field
    
    3. 创建hash表,然后同时创建多个 field-value 对到hash表中
    hmset key field1 value1 field2 value2...
    
    4. 查看hash表中多个字段的值
    hmget ket field1 field2...
    
    5. 查看所有hash表中的字段和值
    hmgetall key
    
    6. 删除一个或多个hash表字段
    hdel key field1 field2...
    
    7. 查看hash表中指定的字段是否存在
    hexists key field
    
    8. 为hash表中指定字段为整数的值自增指定大小,如果不是整数类型,则会报错
    hincrby key field increment
    
    9. 为hash表中指定字段为小数的值自增指定大小,如果不是小数类型,则会报错
    hincrbyfloat key field increment
    
    10. 查看hash表中所有的字段
    hkeys key
    
    11. 查看hash表中所有的值
    hvals key
    
    12. 查看hash表中字段的数量
    hlen key
    
    13. 在指定hash表中创建新的 field-value 对,只有当字段不存在才可创建
    hsernx ket field value
    
    ...
    

    列表命令

    列表中的值是可以重复的

    1. 创建列表与插入数据至指定列表的最左边
    lpush key value1 value2...
    
    2. 查看指定列表行数的数据,从哪开始(start),到哪结束(stop)
    lrange ket start stop
    
    3. 将value插入到列表的最左边,但是如果列表不存在的话就会失败
    lpushx key value value2...
    
    4. 创建列表与插入数据至指定列表的最右边
    rpush key value1 value2...
    
    5. 将value插入到列表的最右边,但是如果列表不存在的话就会失败
    rpush key value1 value2...
    
    6. 移除从头到尾最先发现的count(数量)个value
    lrem key count value
    
    7. 获取列表的长度
    llen key
    
    8. 根据指定index(下表)获取对应的value,下标从0开始
    lindex key index
    
    9. 在指定的pivot(列表中的某个值)左边添加一个值,如果找不到pivot则返回-1,如果是空列表则返回0
    linsert key before pivot value
    
    10. 修改指定index下标的值,下标从0开始
    lset key index value
    
    11. 获取指定区域的值,从start开始,srop结束
    lrange key start srop
    
    12. 截取指定区域的值,其他的值以删除处理,也是从start开始,srop结束
    ltrim key start stop
    
    ...
    

Redis事务

Redis 中的事务和 Mysql 事务没有太大的区别,最大的区别就是 Redis 没有回滚这个概念,Redis 事务中的命令至少要两条。

  1. multi 开启事务,开启事务之后写的命令并不会马上执行,而是把命令添加到队列里面,执行exec才会执行这些命令

  2. exec 执行事务中的所有命令

  3. discard 取消事务

正常执行事务

  1. multi 开启事务

  2. set key value 把这条命令加入到等待队列

  3. set key value 把这条命令加入到等待队列

  4. exec 执行等待队列里面的命令,提交事务

手动取消事务

  1. multi 开始事务

  2. set key value 把这条命令加入到等待队列

  3. set key value 把这条命令加入到等待队列

  4. discard 取消事务

主动放弃事务

  1. multi 开始事务

  2. set key value 把这条命令加入到等待队列

  3. incr key1 key2 现在这条命令在语法上是错误的,因为incr后面只能接一个参数,所以语法上就出现了错误(这就相当于检查时异常),所以这条命令没有执行Redis就知道已经错了,这时事务就会主动放弃

  4. exec 提示事务已经放弃,因为前面出现了错误

注: 只要语法错误就会主动放弃事务!

正常提交事务

  1. multi 开始事务

  2. set key value 把这条命令加入到等待队列

  3. incr key 这个命令在语法上是对的,只有这个命令执行才会发现错误,相当于(运行时异常)。Redis设计初衷认为,像这种错误在实际的项目上线之后是不会发生的,只有在开发阶段可能会出现,但是开发阶段发现问题就已经解决了。所以既然在实际生产环境中不会出现这种问题,那就不需要解决。所以这句命令执行的时候报错,不影响事务的执行

  4. exec 事务正常提交

注: 此处出现的错误是因为incr的key值为字符串类型,而incr为整数自增所以会发生错误!

观察机制(Wacth)

       watch机制,用于监视一个或者多个key,跟踪key的value的修改情况。如果监视的key的值在事务 exec 执行之前被修改了,这个时候事务就会被取消!

  1. watch key1 key2… 表示监视key1和key2
  2. unwatch 取消所有监视的key

watch机制让redis事务变得有限制了。事务能提交,一定是在事务执行之前,保证被监视的key的值没有发生过任何修改。 如果有修改事务就会被取消!

如果监视一个设置了存活时间的key,这个key即使已经不存活了,事务不会被影响,正常执行!

示例
  1. 打开两个客户端,分别为A和B

  2. A:watch name 在A客户端监视k1

  3. A:multi

  4. A:set name laoyang

  5. B:set name hei 在B客户端修改了A客户端监视的key的值,所以A客户端中的事务就已经取消了

  6. A:exec 所以这个时候在去提交事务就不会有任何的作用了,因为事务已经被取消了!

在事务提交前如果在其他地方修改了key的value然后在提交事务就不会有任何的反应了

取消监视key的方法
  1. 手动输入unwatch命令
  2. 关闭当前客户端
  3. 事务执行完,不管成功还是失败,都会取消监视的key

Redis持久化

       Redis的数据是存储在内存中,内存是瞬时的。如果系统或者Redis发生故障,都可能导致数据丢失。所以为了解决这个问题,Redis提供了两种方式进行数据持久化存储,分别是RDB和AOF。

RDB

       Redis DataBase(RDB),通过指定时间的间隔,将内存中的数据集快照写入磁盘中,恢复数据的时候就是将快照文件直接在读到内存中。RDB存储的文件是二进制的,只有一个文件,默认就是dump.rdb这个文件。

如果要做RDB的数据持久化,只需要修改redis.windows.conf这个文件,RDB是默认开启的!

redis.windows.conf 文件配置(使用Ctrl+F可以快速找到需要修改的部位):

  1. RDB文件名:默认为dump.rdb

    dbfilename dump.rdb

  2. RDB存放位置:默认是在当前目录

    dir ./

  3. 配置RDB生成快照的时间间隔策略,这个策略可以配置多个,但是也不要配置太多!只要其中的一个策略被触发,就会执行一次快照

    save 900 1         在900秒内有一条数据被修改就会触发
    save 300 10       在300秒内有十条数据被修改就会触发
    save 60 10000   在60秒内有一万条数据被修改就会触发

小结: RDB方式有数据丢失的风险,RDB存放在二进制的文件,二进制就意味着恢复数据的速度快(比 AOF 要快),适合做缓存(因为缓存追求的就是速度),缓存数据丢失也没什么问题,因为数据还有一份完整的在你的关系数据库中。

AOF

       Append-Only File(AOF) ,Redis每次接收到一条修改的命令,就会把这条命令写到一个AOF文件中(只记录写操作,不记录读操作)。所以Redis重启时,它就会通过执行AOF文件中的所有命令来恢复数据。

AOF文件里面记录的命令是完全按照RESP协议的格式来记录的,每次输入一条修改命令,就会往AOF文件中追加一条命令!

如果要做AOF的数据持久化,也是只需要修改redis.windows.conf这个文件,AOF是默认关闭。

redis.windows.conf 文件配置:

  1. 开启AOF持久化机制,默认是no,需要修改成yes

    appendonly yes

  2. AOF文件名,默认是appendonly.aof

    appendfilename “appendonly.aof”

  3. AOF文件存放位置,默认是在当前目录

    dir ./

  4. 配置向AOF文件写命令数据的策略,默认是everysec

    appendfsync always 表示每写一条修改命令,就会马上写入到AOF文件中,很安全但是速度没那么快。

    appendfsync everysec 表示每一秒钟写一次到AOF文件中,相对安全,速度也相对较快。

    appendfsync no 表示Redis不主动去操作,而是交给操作系统,操作系统会30秒执行一次,把命令写入到AOF文件中,不安全但是速度快。

  5. 设置重写的最小AOF文件大小。默认是64M,如果AOF文件大于64M时,开始整理AOF文件,去掉无用的操作命令,从而减小AOF文件大小

    auto-aof-rewrite-min-size 64mb

主从复制-读写分离

主从复制-读写分离: 由一个主Redis和多个从Redis组成。主Redis主要负责写数据(也可以读数据)。从Redis就是负责读数据(不能写数据),如果其中的一台服务器上的数据更新了,则会自动更新其它的所有服务器的数据,这就是主从复制。

       RDB和AOF两种持久化都是在一个Reids中(一台服务器),如果这台服务器出现了故障(比如硬盘坏死),那意味着这个Redis中的数据就永久丢失了。为了避免这种情况发生的概率,采用主从复制(有多个服务器,其中一个服务器坏死也没问题,因为其它服务器上还有数据)。

模拟主从复制

一主两从的结构(一个服务模拟一个服务器,也就是一个Redis)

主Redis:6379

从Redis1:6380

从Redis2:6381

实现步骤:

  1. 复制3份Redis,默认3个服务器,在把3份Redis分别修改对应的端口号
    在这里插入图片描述

    端口号在每个redis的redis.windows.conf文件中修改(默认为port 6379)

  2. 把从Reids的配置文件修改 slaveof masterip masterport
    在这里插入图片描述
    在下面添加主Redis的 IP 地址与端口号

  3. 安装并启动3个Redis服务

  4. info replication 查看主从关系是否正确

  5. 往主Redis中写数据,看看是否同步到从Redis中

从服务器挂掉

       在主从关系中,如果从服务器挂掉,对整个系统来说没有什么问题。因为主Redis照常负责写,其它的从Redis照常负责读,如果所有的从Redis都挂掉了,这个时候系统也能正常运行,因为主Redis照常进行读写,只是性能会有很大的影响!

shutdown 模拟主redis挂掉(如果是使用可视图化工具使用该命令的话会卡住,重启即可)

主服务器挂掉

       在主从关系中,如果主服务器挂掉,对整个系统来说,只是不能写,从Redis还是照常进行读操作。这时只要把一个从Redis提升为主Redis,把其它的从Redis挂至这个新的主Redis中即可。

解决方案一:冷处理(容灾处理)

       当主Redis挂掉之后,需要手动把一个从Redis提升为主Redis,把其它的从Redis挂至这个新的主Redis中,这就是冷处理。

步骤:

  1. shutdown 模拟主redis挂掉

  2. slaveof no one 把一个从Redis提升为主Redis

  3. slaveof masterIP masterPort 把其它的从Redis挂至这个主Redis中
    在这里插入图片描述

解决方案二:哨兵(Sentinel)

       Sentinel哨兵是Redis官方提供的高可用方案。用来监控多个Redis服务的运行情况。Redis Sentinel是一个运行在特殊模式下的Redis服务。

Sentinel的三个任务:

  1. 监控:每个Sentinel都会不定时的往主Redis中发送ping命令,看看有没有正常工作。Sentinel一定是奇数个,最少3个,因为出现故障的判断是根据哨兵投出的票数所决定,遵循少数服从多数的原则。如果监控的哨兵投票超过半数,就认定主Redis没有正常在工作了。

  2. 提醒

  3. 自动故障转移

实现步骤:

  1. 创建三个哨兵文件,模拟三个哨兵,sentinel26379.conf,sentinel26380.conf,sentinel26381.conf

  2. 编写哨兵文件,把端口和监控的主Redis信息进行相应的修改

    port 26379
    # 监控的是哪一个主redis,后面的2表示哨兵几票可通过,因为我这里只弄了三个哨兵所以两票即可
    sentinel monitor mymaster 127.0.0.1 6380 2
    sentinel down-after-milliseconds mymaster 5000
    # 超时时间
    sentinel failover-timeout mymaster 15000
    

    其余两个只需要更改port 26379端口号即可

  3. 启动3个哨兵在存放sentinel26379.conf等文件的文件夹地址栏中进入cmd页面然后输入以下命令:

    第一个
    redis-server.exe sentinel26379.conf --sentinel
    
    第二个
    redis-server.exe sentinel26380.conf --sentinel
    
    第三个
    redis-server.exe sentinel26381.conf --sentinel
    

    温馨提示: 需要启动三个cmd页面,然后在每个页面输入不同的文件名进行启动!

  4. 三个哨兵就会实时监控主Redis的运行情况,整个主从结构哨兵都是全程监控的,一旦出现主Redis挂掉,哨兵就会开始故障转移!

注: Linx版本的Redis有自带的哨兵文件,而windows版的redis没有。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值