Redis(四)事务和锁机制及持久化操作

事务和锁机制

事务

Redis的事务定义

Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

Redis事务的主要作用就是串联多个命令从而防止别的命令插队

在这里插入图片描述

Multi、Exec、discard

从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行。

直到输入Exec后,Redis会将之前命令队列中的命令依次执行。

组队的过程中可以通过discard来放弃组队。

在这里插入图片描述

组队成功,且提交成功

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) OK

discard放弃组队

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> discard
OK
127.0.0.1:6379> 

事务的错误处理

组队阶段报错,提交全部失败

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.

组队阶段成功,提交时有失败

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> incr k3
QUEUED
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) (error) ERR value is not an integer or out of range
3) OK

总结:

  1. 组队中的某个命令出现了报告错误,执行时整个的所有队列都会被取消。
  2. 如果执行阶段某个命令报出了错误,只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。

事务冲突的问题

案例

同一张银行卡,里面有一万块钱,此时有三个人(你的前女友,女友和你)同时想要付款:

一个请求想给金额减8000

一个请求想给金额减5000

一个请求想给金额减1000

(现在看来前女友是最狠的)

在这里插入图片描述
很明显,这样操作是不当的,满足不了初衷——用薪创造快乐,所以引入悲观锁和乐观锁来解决这个冲突问题。

悲观锁

悲观锁(Pessimistic Lock),每次去拿数据的时候都认为别人会修改数据,所以在每次拿数据的时候都会上锁,这样别人想拿数据就会阻塞直到别人自己拿到锁。传统的关系型数据库里用到了很多这种锁机制,比如行锁、表锁等,读锁、写锁等,都是在做操作之前先上锁。

在这里插入图片描述

乐观锁

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

在这里插入图片描述

watch key [key…] 演示乐观锁

在执行multi时,先执行watch key1 [key2],可以监视一个或多个key,如果在事务执行之前(或这些)key被其他命令所改动,那么事务将被打断。
在这里插入图片描述

unwatch

取消WATCH命令对所有key的监视。

如果在执行WATCH命令之后,EXEC命令或DISCARD命令先被执行了的话,那么就不需要再执行UNWATCH了。

Redis事务三特性
  1. 单独的隔离操作
    事务中所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
  2. 没有隔离级别的概念
    队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行。
  3. 不保证原子性
    事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚。

秒杀案例

电商平台秒杀活动:给定库存,用户进行秒杀,抢完活动停止。

基本功能实现(不考虑并发)

<%@ 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" method="post">
	<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(){
		let url=$("#msform").attr("action");
		$.post(url,$("#msform").serialize(),function(data){
			if(data == "false"){
				alert("抢光了");
				$("#miaosha_btn").attr("disabled",true);
			}
		});
	})
})
</script>
</html>
package com.atguigu.seckill;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Random;

/**
 * @version 1.0
 * @Description
 * @Author 月上叁竿
 * @Date 2022-03-24 9:22
 **/
@WebServlet("/doseckill")
public class SecKillServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String userId = new Random().nextInt(50000) + "";
        String prodId = request.getParameter("prodId");

        boolean isSuccess = SecKill_redis.doSecKill(userId,prodId);
        response.getWriter().print(isSuccess);
    }
}
package com.atguigu.seckill;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Transaction;

import java.io.IOException;
import java.util.List;

/**
 * @version 1.0
 * @Description
 * @Author 月上叁竿
 * @Date 2022-03-24 9:32
 **/
public class SecKill_redis {
    public static boolean doSecKill(String uid, String prodId) throws IOException{
        // 1.非空判断  判断传来的用户id和商品id是否为空 如果为空 不执行
        if (uid == null || prodId == null)
            return false;
        // 2.连接redis
        JedisPool jedisPool = JedisPoolUtil.getJedisPoolInstance();
        Jedis jedis = jedisPool.getResource();
        // 3.拼接相关的key
        // 库存key
        String kcKey = "sk:"+prodId+":qt";
        // 秒杀成功的用户的key
        String userKey = "sk:" + prodId + ":user";
        // 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) <= 0){
            System.out.println("秒杀已结束,感谢您的参与!");
            jedis.close();
            return false;
        }
        // 7 秒杀过程
        jedis.decr(kcKey);
        jedis.sadd(userKey,uid);
        System.out.println("恭喜您,秒杀成功!");
        jedis.close();
        return true;
    }
}

考虑并发情况

使用工具ab进行并发模拟,CentOs 7需要手动进行安装。

yum install -y httpd-tools

安装好后,新建postfile文件模拟表单提交参数,以&符号结尾,存放到当前目录。

vim postfile
prodid=0101&

使用ab工具模拟1000条请求,每次并发100条

ab -n 1000 -c 100 -p postfile -T application/x-www-form-urlencoded http://192.168.1.113:8080/kill/doseckill

无论是从IDEA,还是Redis客户端,我们都能捕捉到很明显的错误

在这里插入图片描述
即超卖问题。

超卖问题及其解决方法

类似于之前提到的事务冲突的案例,我们是用悲观锁和乐观锁来解决冲突的。

Redis自身提供了乐观锁的机制,因此我们使用乐观锁来解决超卖问题。

在这里插入图片描述
使用watch开启乐观锁,并开启事务操作。

package com.atguigu.seckill;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Transaction;

import java.io.IOException;
import java.util.List;

/**
 * @version 1.0
 * @Description
 * @Author 月上叁竿
 * @Date 2022-03-24 9:32
 **/
public class SecKill_redis {
    public static boolean doSecKill(String uid, String prodId) throws IOException{
        // 1.非空判断  判断传来的用户id和商品id是否为空 如果为空 不执行
        if (uid == null || prodId == null)
            return false;
        // 2.连接redis
        JedisPool jedisPool = JedisPoolUtil.getJedisPoolInstance();
        Jedis jedis = jedisPool.getResource();
        // 3.拼接相关的key
        // 库存key
        String kcKey = "sk:"+prodId+":qt";
        // 秒杀成功的用户的key
        String userKey = "sk:" + prodId + ":user";
        // 4 获取库存 若库存为null,表示秒杀尚未开始
        // watch监视库存,开启乐观锁
        jedis.watch(kcKey);

        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) <= 0){
            System.out.println("秒杀已结束,感谢您的参与!");
            jedis.close();
            return false;
        }

        // 7 秒杀过程
        // 开启事务操作
        Transaction multi = jedis.multi();
        multi.decr(kcKey);
        multi.sadd(userKey,uid);
        List<Object> list = multi.exec();

        if (list == null || list.size() == 0){
            System.out.println("秒杀失败QAQ...");
            jedis.close();
            return false;
        }

        System.out.println("恭喜您,秒杀成功!");
        jedis.close();
        return true;
    }
}

连接超时问题的解决方案

通过Jedis连接池即可解决。

package com.atguigu.seckill;

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.36.128", 6379, 60000 );
				}
			}
		}
		return jedisPool;
	}

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

使用连接池可以节省每次连接redis服务带来的消耗,把连接好的实例反复利用。

连接池通过参数管理连接的行为:

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

已经秒杀光,但仍然有库存的情况

在这里插入图片描述
这种情况是由于乐观锁导致很多请求都失败,先点秒杀的没有秒到,后点的反而可能秒到了。

这里通过LUA解决库存遗留问题。

那么LUA是什么呢?

LUA脚本

LUA是一个小巧的脚本语言,LUA脚本可以很容易的被C/C++代码调用,也可以反过来调用C/C++的函数,LUA并没有提供强大的库,一个完整的LUA解释器不过200k,所以LUA不适合作为开发独立应用程序的语言,而是作为嵌入式脚本语言。

很多应用程序、游戏使用LUA作为自己的嵌入式脚本语言,以此来实现可配置性、可扩展性。

LUA脚本在Redis中的优势

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

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

但是注意redis的LUA脚本功能,只有在Redis2.6以上的版本才可以使用。

利用LUA脚本淘汰用户,解决库存遗留问题。

注:利用LUA脚本解决争抢问题,实际上是Redis利用其单线程的特性,用任务队列的方式解决多任务并发问题

package com.atguigu.seckill;

import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

public class SecKill_redisByScript {
	
	private static final org.slf4j.Logger logger =LoggerFactory.getLogger(SecKill_redisByScript.class) ;

	public static void main(String[] args) {
		JedisPool jedispool =  JedisPoolUtil.getJedisPoolInstance();
 
		Jedis jedis=jedispool.getResource();
		System.out.println(jedis.ping());
		
		Set<HostAndPort> set=new HashSet<HostAndPort>();

	//	doSecKill("201","sk:0101");
	}
	
	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();

		 //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;
	}
}

持久化

Redis提供了两种不同形式的持久化方式。

  1. RDB(Redis DataBase)
  2. AOF(Append Of File)

RDB

RDB指在指定的时间间隔内将内存中的数据集快照写入磁盘。

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

RDB是默认的持久化方式。

Fork

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

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

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

触发机制

save

该命令会阻塞当前Redis服务器,执行save命令期间,Redis不能处理其他命令,直到RDB过程完成。

在这里插入图片描述

bgsave

具体操作是Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。基本上Redis内部所有的RDB操作都是采用bgsave命令。Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。
在这里插入图片描述

自动触发 — 由配置文件完成

自动触发是由我们的配置文件来完成的。在redis.conf配置文件中,里面有如下配置可以去设置。

  • save

用来配置触发Redis的RDB持久化条件,也就是什么时候将内存中的数据保存到硬盘

比如:“save m n”,表示m秒内数据集存在n次修改时,自动触发bgsave。

默认配置如下:

在这里插入图片描述
不需要持久化,就可以注释掉所有的save行来停用保存功能。

  • stop-writes-on-bgsave-error

默认值为yes。当启用了RDB且最后一次后台保存数据失败,Redis是否停止接收数据。这会让用户意识到数据没有正确持久化到磁盘上,否则没有人会注意到灾难发生了。如果Redis重启了,那么又可以重新开始接收数据了。

  • rdbcompression

默认值是yes。在存储到磁盘中的快照,可以设置是否进行压缩存储。

  • rdbchecksum

默认值是yes。在存储快照后,我们还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以将其关闭。

  • dbfilename

设置快照的文件名,默认是dump.rdb。

  • dir

设置快照文件的存放路径,这个配置项一定是个目录,而不能是文件名。

save 和 bgsave

命令savebgsave
IO类型同步异步
阻塞是(阻塞发生在fork)
复杂度O(n)O(n)
优点不会消耗额外内存不阻塞客户端命令
缺点阻塞客户端命令需要fork,消耗内存
RDB的优劣

优势

  1. RDB文件紧凑,全量备份,非常适合用于进行备份和灾难恢复。
  2. 生成RDB文件的时候,redis主进程会fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘IO操作,从而节省磁盘空间。
  3. RDB在恢复大数据集时的速度比AOF的恢复速度要快。

劣势

  1. Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑。
  2. 虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是消耗性能。
  3. 在备份周期且在一定间隔时间做一次备份,如果Redis意外挂掉的话,就会丢失最后一次快照后的所有修改。

AOF

AOF以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来,只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,即redis重启根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。

AOF持久化流程

  1. 客户端的请求写命令会被append追加到AOF缓冲区内。
  2. AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中。
  3. AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量。
  4. Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的。

AOF默认不开启,若要开启,将redis.conf中的appendonly改为yes即可。
在这里插入图片描述
默认文件名称为:appendonly.aof,文件的保存路径与RDB一致,同是启动命令时所在的目录下。

需要注意的是,RDB是Redis默认使用的持久化方式,当开启AOF时,相当于两个持久化方式同时开启,此时系统默认取AOF中的数据。

触发方式

always

始终同步,每次Redis的写入都会立刻记入日志。性能较差但数据完整性好。

everysec

每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能会丢失。

no

redis不主动进行同步,而是把同步时机交给操作系统。

AOF恢复

正常恢复

与RDB相同,拷贝appendonly.aof作为备份文件,需要恢复时拷贝到工作目录下即可。

cp appendonly.aof appendonly.aof.bak

异常恢复

当AOF文件损坏,此时无法正常连接到Redis客户端。需要通过

redis-check-aof --fix appendonly.aof

进行恢复,之后重启redis进行加载。

AOF Rewirte压缩

AOF采用文件追加的方式,文件是会越来越大的。为了避免出现这种情况,新增了重写机制,当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集。Redis提供了命令bgrewriteaof,将内存中的数据以命令的方式保存到临时文件中,同时会fork出一条新进程来将文件重写。

Redis 4.0后的重写,是把rdb的快照,以二进制的形式附在新的aof的头部作为已有的历史数据,替换掉原来的操作。

no-appendfsync-on-rewrite

如果no-appendfsync-on-rewrite=yes,不写入aof文件只写入缓存,用户请求不会阻塞,但是在这段时间里如果宕机就会丢失这段时间内的缓存数据。即降低数据的安全性,但是提高了性能

如果no-appendfsync-on-rewrite=no,还是会将数据存放到磁盘中,但是遇到重写操作,可能会发生阻塞。即降低性能,但是数据是安全的

触发机制,何时进行重写

Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次重写后文件大小的一倍且文件大于64M时触发。

重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定负担的,因此要设定Redis满足一定条件才会进行重写

auto-aof-rewrite-percentage:设置重写的基准值,文件达到100%时开始重写,即文件是原来重写文件大小的两倍时触发。

auto-aof-rewrite-min-size:设置重写的基准值,最小文件64MB。达到这个值开始重写。

Rewrite流程

  1. bgrewriteaof触发重写,判断当前是否有bgsave或bgrewriteaof在运行,如果有,则等待该命令结束后再继续执行。
  2. fork子进程执行重写操作,保证主进程不会被阻塞。
  3. 子进程遍历redis内存中的数据到临时文件,客户端的写请求同时写入aof_buf缓冲区和aof_rewrite_buf重写缓冲区保证原AOF文件完整以及新AOF文件生成期间的新的数据修改动作不会丢失。
  4. 1 子进程写完新的AOF文件后,向主进程发信号,父进程更新统计信息。
    2 主进程把aof_rewrite_buf中的数据写入到新的AOF文件。
  5. 使用新的AOF文件覆盖旧的AOF文件,完成AOF重写。

在这里插入图片描述

AOF的优劣

优势

  1. 备份机制更加稳健,丢失数据概率更低。
  2. 可读的日志文本,通过操作AOF修复,可以处理误操作。

劣势

  1. 比起RDB占用更多的磁盘空间。
  2. 恢复备份速度慢。
  3. 每次读写都同步的话,性能较差。
  4. 存在bug,造成不能恢复。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值