redis学习总结(中)——事务持久化和主从复制(代码片段)

AC_Jobim AC_Jobim     2022-12-09     607

关键词:

一、Redis的事务操作

  • redis事务就是一个命令执行的队列,将一系列预定义命令包装成一个整体(一个队列)。当执行时,一次性按照添加顺序依次执行,中间不会被打断或者干扰

1.1 事务的操作和错误处理

事务的操作multi、exec、discard:

  1. 开启事务multi
    设定事务的开启位置,此指令执行后,后续的所有指令均加入到事务中

  2. 执行事务exec
    设定事务的结束位置,同时执行事务。与multi成对出现,成对使用
    注意:加入事务的命令暂时进入到任务队列中,并没有立即执行,只有执行exec命令才开始执行

  3. 取消事务discard
    终止当前事务的定义,发生在multi之后,exec之前

即:从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。组队的过程中可以通过discard来放弃组队。

在这里插入图片描述

事务的错误处理:

  • 组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。
    在这里插入图片描述

    在这里插入图片描述

  • 如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。
    在这里插入图片描述
    在这里插入图片描述

1.2 Watch锁

watch锁是一种乐观锁的概念:

乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。

  • watch
    在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,如果在执行exec前如果key被别的线程操作了,则终止事务执行

  • unwatch
    取消 WATCH 命令对所有 key 的监视。
    如果在执行 WATCH 命令之后,EXEC 命令或DISCARD 命令先被执行了的话,那么就不需要再执行UNWATCH 了。

在这里插入图片描述

1.3 Redis_事务_秒杀案例

Redis中记录商品的库存数量秒杀成功者清单
在这里插入图片描述

1.3.1 使用事务(解决超卖)+连接池(解决超时问题)

存在的问题:

  • 问题一:超卖问题

    使用事务(乐观锁)解决

  • 问题二:链接超时问题

    解决:使用连接池

    jedis连接资源的创建与销毁是很消耗程序性能,所以jedis为我们提供了jedis的池化技术,jedisPool在创建时初始化一些连接资源存储到连接池中,使用jedis连接资源时不需要创建,而是从连接池中获取一个资源进行redis的操作,使用完毕后,不需要销毁该jedis连接资源,而是将该资源归还给连接池,供其他请求使用。

    常用参数:

    • MaxTotal:控制一个pool可分配多少个jedis实例,通过pool.getResource()来获取;如果赋值为-1,则表示不限制;如果pool已经分配了MaxTotal个jedis实例,则此时pool的状态为exhausted。
    • maxIdle:控制一个pool最多有多少个状态为idle(空闲)的jedis实例;
    • MaxWaitMillis:表示当borrow一个jedis实例时,最大的等待毫秒数,如果超过等待时间,则直接抛JedisConnectionException;
    • testOnBorrow:获得一个jedis实例的时候是否检查连接可用性(ping());如果为true,则得到的jedis实例均是可用的;

代码示例

Redis连接池代码:

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class JedisPoolUtil 
	private static volatile JedisPool jedisPool = null;

	private JedisPoolUtil() 
	

	public static JedisPool getJedisPoolInstance() 
		if (null == jedisPool) 
			synchronized (JedisPoolUtil.class) 
				if (null == jedisPool) 
					JedisPoolConfig poolConfig = new JedisPoolConfig();
					poolConfig.setMaxTotal(200);
					poolConfig.setMaxIdle(32);
					poolConfig.setMaxWaitMillis(100*1000);
					poolConfig.setBlockWhenExhausted(true);
					poolConfig.setTestOnBorrow(true);  // ping  PONG
				 
					jedisPool = new JedisPool(poolConfig, "192.168.2.4", 6379, 60000 );
				
			
		
		return jedisPool;
	

	public static void release(JedisPool jedisPool, Jedis jedis) 
		if (null != jedis) 
			jedisPool.returnResource(jedis);
		
	


Servlet代码:

public class SecKillServlet extends HttpServlet 

    private static final long serialVersionUID = 1L;

    public SecKillServlet() 
        super();
    


    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException 
        String userid = new Random().nextInt(50000) +"" ;
        String prodid = req.getParameter("prodid");

        boolean isSuccess=SecKill_redis.doSecKill(userid,prodid);
//        boolean isSuccess= SecKill_redisByScript.doSecKill(userid,prodid);
        resp.getWriter().print(isSuccess);
    


Redis操作代码:

public class SecKill_redis 

    //秒杀过程
    public static boolean doSecKill(String uid,String prodid) throws IOException 
        //1 uid和prodid非空判断
        if(uid == null || prodid == null) 
            return false;
        

        //2 连接redis
        //通过连接池得到jedis对象
        JedisPool jedisPool = JedisPoolUtil.getJedisPoolInstance();
        Jedis jedis = jedisPool.getResource();

        //3 拼接key
        // 3.1 库存key
        String kcKey = "sk:"+prodid+":qt";
        // 3.2 秒杀成功用户key
        String userKey = "sk:"+prodid+":user";

        //监视库存
        jedis.watch(kcKey);

        //4 获取库存,如果库存null,秒杀还没有开始
        String kc = jedis.get(kcKey);
        if(kc == null) 
            System.out.println("秒杀还没有开始,请等待");
            jedis.close();
            return false;
        

        // 5 判断用户是否重复秒杀操作
        if (jedis.sismember(userKey, uid)) 
            System.out.println("已经秒杀成功了,不能重复秒杀");
            jedis.close();
            return false;
        

        //6 判断如果商品数量,库存数量小于1,秒杀结束
        if (Integer.parseInt(kc) < 1) 
            System.out.println("秒杀已经结束了");
            jedis.close();
            return false;
        

        //7 秒杀过程
        //使用事务
        Transaction multi = jedis.multi();
        //组队操作
        //7.1 库存-1
        multi.decr(kcKey);
        //7.2 把秒杀成功用户添加清单里面
        multi.sadd(userKey,uid);
        //执行
        List<Object> results = multi.exec();
        if (results == null || results.size() == 0) 
            System.out.println("秒杀失败了....");
            jedis.close();
            return false;
        

        System.out.println("秒杀成功了..");
        jedis.close();
        return true;
    


前端发起请求

<%@ page language="java" contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <title>Insert title here</title>
</head>
<body>
<h1>iPhone 13 Pro !!!  1元秒杀!!!
</h1>


<form id="msform" action="$pageContext.request.contextPath/doseckill" enctype="application/x-www-form-urlencoded">
  <input type="hidden" id="prodid" name="prodid" value="0101">
  <input type="button"  id="miaosha_btn" name="seckill_btn" value="秒杀点我"/>
</form>

</body>
<script  type="text/javascript" src="$pageContext.request.contextPath/script/jquery/jquery-3.1.0.js"></script>
<script  type="text/javascript">
  $(function()
    $("#miaosha_btn").click(function()
      var url=$("#msform").attr("action");
      $.post(url,$("#msform").serialize(),function(data)
        if(data=="false")
          alert("抢光了" );
          $("#miaosha_btn").attr("disabled",true);
        
       );
    )
  )
</script>
</html>

1.3.2 使用LUA脚本解决库存依赖问题

问题:已经秒光,可是还有库存。原因,就是乐观锁导致很多请求都失败。先点的没秒到,后点的可能秒到了。

解决:

将复杂的或者多步的redis操作,写为一 个脚本,一次提交给redis执行,减少反复连接redis的次数。提升性能。

LUA脚本是类似redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作

但是注意redis的lua脚本功能,只有在Redis 2.6以上的版本才可以使用。

通过lua脚本解决争抢问题,实际上是redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题

LUA脚本

local userid=KEYS[1]; 
local prodid=KEYS[2];
local qtkey="sk:"..prodid..":qt";
local usersKey="sk:"..prodid.":usr'; 
local userExists=redis.call("sismember",usersKey,userid);
if tonumber(userExists)==1 then 
  return 2;
end
local num= redis.call("get" ,qtkey);
if tonumber(num)<=0 then 
  return 0; 
else 
  redis.call("decr",qtkey);
  redis.call("sadd",usersKey,userid);
end
return 1;

Redis使用LUA脚本代码

public class SecKill_redisByScript 
	
	private static final  org.slf4j.Logger logger =LoggerFactory.getLogger(SecKill_redisByScript.class) ;
	
	static String secKillScript ="local userid=KEYS[1];\\r\\n" + 
			"local prodid=KEYS[2];\\r\\n" + 
			"local qtkey='sk:'..prodid..\\":qt\\";\\r\\n" + 
			"local usersKey='sk:'..prodid..\\":usr\\";\\r\\n" + 
			"local userExists=redis.call(\\"sismember\\",usersKey,userid);\\r\\n" + 
			"if tonumber(userExists)==1 then \\r\\n" + 
			"   return 2;\\r\\n" + 
			"end\\r\\n" + 
			"local num= redis.call(\\"get\\" ,qtkey);\\r\\n" + 
			"if tonumber(num)<=0 then \\r\\n" + 
			"   return 0;\\r\\n" + 
			"else \\r\\n" + 
			"   redis.call(\\"decr\\",qtkey);\\r\\n" + 
			"   redis.call(\\"sadd\\",usersKey,userid);\\r\\n" + 
			"end\\r\\n" + 
			"return 1" ;
			 
	static String secKillScript2 = 
			"local userExists=redis.call(\\"sismember\\",\\"sk:0101:usr\\",userid);\\r\\n" +
			" return 1";

	public static boolean doSecKill(String uid,String prodid) throws IOException 

//		JedisPool jedispool =  JedisPoolUtil.getJedisPoolInstance();
//		Jedis jedis=jedispool.getResource();
		Jedis jedis = new Jedis("192.168.2.4", 6379);
		 //String sha1=  .secKillScript;
		String sha1=  jedis.scriptLoad(secKillScript);
		Object result= jedis.evalsha(sha1, 2, uid,prodid);

		  String reString=String.valueOf(result);
		if ("0".equals( reString )  ) 
			System.err.println("已抢空!!");
		else if("1".equals( reString )  )  
			System.out.println("抢购成功!!!!");
		else if("2".equals( reString )  )  
			System.err.println("该用户已抢过!!");
		else
			System.err.println("抢购异常!!");
		
		jedis.close();
		return true;
	

1.3.3 使用工具ab来模拟并发

CentOS6 默认安装
CentOS7需要手动安装

安装ab工具:yum install httpd-tools

执行代码:

ab -n 2000 -c 200 -k -p ~/postfile -T application/x-www-form-urlencoded http://192.168.81.1:8080/seckill/doseckill
-n 连接数 -c 并发数

在这里插入图片描述

二、Redis持久化

Redis高性能是由于其将所有数据存储在了内存中,为了使Redis在重启之后仍能保证数据不丢失,需要将数据从内存中同步到硬盘中,这一过程就是持久化。Redis支持两种方式的持久化,一种是 RDB方式,一种是 AOF方式。可以单独使用其中一种或将二者结合使用。

  • RDB持久化(默认支持,无需配置)
    该机制是指在指定的时间间隔内将内存中的数据集快照写入磁盘。快照(Snapshot)也称为RDB持久化方式
  • AOF持久化
    该机制将以日志的形式记录服务器所处理的每一个写操作,在Redis服务器启动之初会读取该文件来重新构建redis数据库,以保证启动后数据库中的数据是完整的。
  • 无持久化
    我们可以通过配置的方式禁用Redis服务器的持久化功能,这样我们就可以将Redis视为一个功能加强版的memcached了。
  • redis可以同时使用RDB和AOF

2.1 RDB持久化方式

2.1.1 RDB持久化特点

是什么?

在指定的时间间隔内将内存中的数据集快照写入磁盘, 也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里

备份是如何执行的?

Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。 整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能 如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失

Fork?

  • Fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程

  • 在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,Linux中引入了“写时复制技术

  • 一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。

2.1.2 快照生成方式

  • 客户端方式: BGSAVESAVE指令
  • 服务器配置自动触发

2.1.2.1 save或者bgsave命令

bgsave命令:

  • 客户端可以使用BGSAVE命令来创建一个快照,当redis服务器接收到客户端BGSAVE命令时,redis会调用fork创建一个子进程,然后子进程负责将快照写入磁盘中,而父进程则继续处理命令请求。即:Redis会在后台异步进行快照操作, 快照同时还可以响应客户端请求

save命令:

  • 客户端还可以使用SAVE命令来创建一个快照,接收到SAVE命令的redis服务器在快照创建完毕之前将不再响应任何其他的命令。即:使用SAVE命令在快照创建完毕之前,redis处于阻塞状态,无法对外服务(写操作)

2.1.2.2 自动触发(重点)

  • 如果用户在redis.conf中设置了save配置选项,redis会在save选项条件满足之后自动触发一次BGSAVE命令, 如果设置多个save配置选项,当任意一个save配置选项条件满足,redis也会触发一次BGSAVE命令
    在这里插入图片描述

  • 表示 900S(15分钟), key发生1次变化, 就触发一次 bgsave命令, 持久化一次

  • 表示300S(5分钟), key发生10次变化, 就触发一次bgsave命令, 持久化一次

  • 表示60S(1分钟), key发生10000次变化, 就触发一次bgsave命令, 持久化一次

上面自动触发的规则: 标明key改变的越频繁, 触发快照持久化到硬盘的时间就越短;

2.1.2.3 服务器接收客户端shutdown指令

  • redis服务器接收到redis客户端发来的shutdown指令关闭服务器时,会执行一个save命令,阻塞所有的客户端,不再执行客户端执行发送的任何命令,并且在save命令执行完毕之后关闭服务器

2.1.3 RDB相关配置

  1. rdb文件名
    在redis.conf中配置文件名称,默认为dump.rdb
    在这里插入图片描述

  2. 配置文件位置
    rdb文件的保存路径,也可以修改。默认为Redis启动时命令行所在的目录下
    dir “/myredis/”
    在这里插入图片描述

  3. stop-writes-on-bgsave-error
    在这里插入图片描述
    后台存储过程中如果出现错误现象,是否停止保存操作。推荐yes.

  4. rdbcompression 压缩文件
    在这里插入图片描述
    对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis会采用LZF算法进行压缩。如果你不想消耗CPU来进行压缩的话,可以设置为关闭此功能,但会使存储的文件变大(巨大)。推荐yes.

  5. rdbchecksum 检查完整性
    在这里插入图片描述
    在存储快照后,还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。推荐yes.

2.1.4 RDB的备份

  1. 将*.rdb的文件拷贝到别的地方
  2. 关闭Redis
  3. 先把备份的文件拷贝到工作目录下 cp dump2.rdb dump.rdb
  4. 启动Redis, 备份数据会直接加载

2.1.5 RDB持久化的优缺点:

优点:

  1. RDB是一个紧凑压缩的二进制文件,存储效率较高
  2. RDB内部存储的是redis在某个时间点的数据快照,非常适合用于数据备份,全量复制等场景
  3. RDB恢复数据的速度要比AOF快很多

应用:服务器中每X小时执行bgsave备份,并将RDB文件拷贝到远程机器中,用于灾难恢复

缺点:

  1. RDB方式无论是执行指令还是利用配置,无法做到实时持久化,具有较大的可能性丢失数据
  2. bgsave指令每次运行要执行fork操作创建子进程,要牺牲掉一些性能
  3. 基于快照思想,每次读写都是全部数据,当数据量巨大时,效率非常低

2.2 Redis持久化之AOF

2.2.1 AOF持久化的特点

  • AOF持久化可以将所

    深入学习redis:主从复制(代码片段)

    ...面的两篇文章中,分别介绍了Redis的内存模型和Redis的持久化。在Redis的持久化中曾提到,Redis高可用的方案包括持久化、主从复制(及读写分离)、哨兵和集群。其中持久化侧重解决的是Redis数据的单机备份问题&#x... 查看详情

    redis学习总结(21)——redis持久化是如何做的?rdb和aof对比分析(代码片段)

    前言Redis要想实现高可用,主要有以下方面来保证:数据持久化主从复制自动故障恢复集群化Redis的高可用保障的基础:数据持久化。因为Redis的主从复制和自动故障恢复,都需要依赖Redis持久化相关的东西。同时,Redis的数据持... 查看详情

    redis6.0高级(代码片段)

    ...的错误处理WATCHkey[key...]命令unwatch命令Redis事务三特性Redis持久化之RDBRDB是什么备份是如何执行的ForkRDB持久化流程dump.rdb文件RDB的优势RDB劣势如何停止Redis持久化之AOFAOF是什么AOF持久化流程AOF启动/修复/恢复AOF同步频率设置Rewrite压... 查看详情

    springboot学习总结二(代码片段)

    ...更丰富的数据结构,例如hashes,lists,sets等,同时支持数据持久化。除此之外,Redis还提供一些类数据库的特性,比如事务,HA,主从库。可以说Redis兼具了缓存系统和数据库的一些特性,因此有着丰富的应用场景。本文介绍Redis在Sp... 查看详情

    深入学习redis主从复制

    ...主要包括:数据冗余:主从复制实现了数据的热备份,是持久化之 查看详情

    redis数据库主从哨兵群集(代码片段)

    ...兵模式、Cluster在Redis中,实现高可用的技术主要包括持久化、主从复制、哨兵和集群持久化持久化是最简单的高可用方法(有些时候会不被归为高可用措施)主要作用是数据备份,也就是将数据存储在硬盘,... 查看详情

    redis学习总结(22)——redis的主从复制是如何做的?复制过程中也会产生各种问题?(代码片段)

    前言如果Redis的读写请求量很大,那么单个实例很有可能承担不了这么大的请求量,如何提高Redis的性能呢?你也许已经想到了,可以部署多个副本节点,业务采用读写分离的方式,把读请求分担到多个副本节点上,提高访问性... 查看详情

    redis学习--key的通用操作移库操作订阅与事务持久化和总结(代码片段)

    ...据库的commitdiscard事务回滚,类似关系数据库的rollback 持久化与总结redis效率快主要是因为存储在内存中,如果服务器出现故障,那么将会丢失数据,于是我们可以讲数据库持久化 1.RDB持久化Redisdatabase修改配置文件save9001#9... 查看详情

    redisredis主从复制+读写分离(代码片段)

    ...制+读写分离1.Redis主从复制+读写分离介绍1.1从数据持久化到服务高可用1.2主从复制1.3如何保证主从数据一致性?1.4为何采用读写分离模式?2.一主两从环境准备2.1配置文件2.2启动Redis3.主从复制原理3.1全量同步3.1.1建... 查看详情

    redis学习总结(23)——redis集群化方案对比:codistwemproxyrediscluster

    ...,为了保证Redis的高可用,主要需要以下几个方面:数据持久化主从复制自动故障恢复集群化我们简单理一下这几个方案的特点,以及它们之间的联系。数据持久化本质上是为了做数据备份,有了数据持久化,当Redis宕机时,我... 查看详情

    redis入门笔记(代码片段)

    ...锁4.4.2、乐观锁4.4.3、WATCH和UNWATCH4.5、Redis事务三特性5、持久化之RDB5.1、是什么5.2、如何执行5.3、Fork5.4、配置及命令5.5、备份恢复5.6、优势劣势6、持久化之AOF6.1、是什么6.2、持久化流程6.3、启动、修复、恢复6.4、AOF同步频率设置... 查看详情

    深入学习redis:持久化

    ...篇文章开始,将依次介绍Redis高可用相关的知识——持久化、复制(及读写分离)、哨兵、以及集群。本文将先说明上述几种技术分别解决了Redis高可用的什么问题;然后详细介绍Redis的持久化技术,主要是RDB和AOF两种持... 查看详情

    redis主从复制最好采用哪种结构

    ...复制总结整理主题 RedisRedis的主从复制策略是通过其持久化的rdb文件来实现的,其过程是先dump出rdb文件,将rdb文件全量传输给slave,然后再将dump后的操作实时同步到slave中。让从服务器(slaveserver)成为主服务器(masterserver)的精... 查看详情

    最详细的一篇关于redis主从复制(代码片段)

    Redis高可用的方案包括持久化、主从复制(及读写分离)、哨兵和集群。其中持久化侧重解决的是Redis数据的单机备份问题(从内存到硬盘的备份);而主从复制则侧重解决数据的多机热备。此外,主从复... 查看详情

    redis主从与哨兵架构-学习

    ...复制数据。master收到PSYNC命令后,会在后台进行数据持久化通过bgsave生成最新的rdb快照文件,持久化期间,master会继续接收客户端的请求,它会把这些可能修改数据集的请求缓存在内存中。当持久化进行完毕以后&#... 查看详情

    深入学习redis:哨兵(代码片段)

    ...):Redis内存模型深入学习Redis(2):持久化深入学习Redis(3):主从复制深入学习Redis(4):哨兵深入学习Redis(5):集群目录一、作用和架构      1.作用      2.架... 查看详情

    java应用xixredis入门

    ...令在一个事务中执行,保证操作的原子性和一致性。支持持久化:Redis可以将数据持久化到磁盘上,以保证数据的可靠性和持久性。支持复制和高可用性:Redis支持主从复制和哨兵机制,可以实现高可用性和负载均衡。Redis的缺点... 查看详情

    redis知识点总结

    ...edis支持数据备份,即主从模式的数据备份。Redis支持数据持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载。而Memcached数据不支持持久化。Redis的速度比Memcached快很多。Memcached是多线程的非阻塞IO复用的网络... 查看详情