Spring AOP 实现自定义注解

自工作后,除了一些小项目配置事务使用过 AOP,真正自己写 AOP 机会很少,另一方面在工作后还没有写过自定义注解,一直很好奇注解是怎么实现他想要的功能的,刚好做项目的时候,经常有人日志打得不够全,经常出现问题了,查日志的才发现忘记打了,所以趁此机会,搜了一些资料,用 AOP + 自定义注解,实现请求拦截,自定义打日志,玩一下这两个东西,以下是自己完的一个小例子,也供需要的同学参考。 注解如下: package cn.bridgeli.demo.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @author bridgeli */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface MyLog { /** * 方法描述 * * @return */ String desc() default ""; } 切面 package cn.bridgeli.demo.annotation; import cn.bridgeli.utils.AuthorizeUtil; import cn.bridgeli.entity.Principal; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; /** * @author bridgeli * 1. 这是一个切面类 */ @Aspect @Component @Slf4j public class MyLogAspect { /** * 2. PointCut表示这是一个切点,@annotation表示这个切点切到一个注解上,后面带该注解的全类名 * 切面最主要的就是切点,所有的故事都围绕切点发生 * logPointCut()代表切点名称 */ @Pointcut("@annotation(cn.bridgeli.demo.annotation.MyLog)") public void logPointCut() { } /** * 3. 环绕通知 * * @param joinPoint * @param myLog * @return */ @Around(value = "logPointCut() && @annotation(myLog)", argNames = "joinPoint,myLog") public Object logAround(ProceedingJoinPoint joinPoint, MyLog myLog) { // 获取方法名 String methodFullPathName = joinPoint.getTarget().getClass().getName() + "#" + joinPoint.getSignature().getName(); // 获取参数 String params = StringUtils.join(joinPoint.getArgs(), ";"); Principal currentUser = AuthorizeUtil.getCurrentUser(); log.info("当前登陆用户:" + (null == currentUser ? "" : currentUser.toString()) + ",进入 [ " + methodFullPathName + " ] 方法, 方法的描述:" + myLog.desc() + ",参数为:" + params); // 继续执行方法 long startTime = System.currentTimeMillis(); Object result = null; try { result = joinPoint.proceed(); } catch (Throwable e) { log.error("切面执行报错,参数:" + params, e); } long elapsed = System.currentTimeMillis() – startTime; log.info("[ " + methodFullPathName + " ] 方法执行结束,返回值为:" + (null == result ? "" : result.toString()) + ",用时:" + elapsed); return result; } } 然后只需要在想使用的地方 @MyLog 就可以了,当然也可以加上 @MyLog(desc = “这是方法描述”),这样打出来的日志还会有方法是做什么的,别人看日志的时候能够一目了然。 ...

March 15, 2020 · 2 min · 248 words · Bridge Li

Java 使用 FFmpeg 处理视频文件示例

Java 使用 FFmpeg 处理视频文件示例 目前在公司做一个小东西,里面用到了 FFmpeg 简单处理音视频,感觉功能特别强大,在做之前我写了一个小例子,现在记录一下。 首先说明,我是在 https://ffmpeg.zeranoe.com/builds/ 这个地方下载的软件,Windows 和 Mac 解压之后即可使用。具体代码如下: package cn.bridgeli.demo; import org.junit.Test; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; /** * @author BridgeLi * @date 2020/2/29 15:40 */ public class FfmpegTest { private static final String OS = System.getProperty("os.name").toLowerCase(); private static final String FFMPEG_PATH = "/Users/bridgeli/ffmpeg-20200216-8578433-macos64-static/bin/ffmpeg"; @Test public void testFfmpeg() { String inputWavFile = "/Users/bridgeli/inputWavFile.wav"; String inputMp3File = "/Users/bridgeli/inputMp3File.mp3"; String inputMp4File = "/Users/bridgeli/inputMp4File.mp4"; String outMergeMp3File = "/Users/bridgeli/outMergeMp3File.mp3"; String outMergeMp3AndMp4File = "/Users/bridgeli/outMergeMp3AndMp4File.mp4"; String outConcatMp3File = "/Users/bridgeli/outConcatMp3File.mp3"; // 拼接 String command = null; if (OS.contains("mac") || OS.contains("linux")) { command = FFMPEG_PATH + " -i " + inputMp3File + " -i " + inputWavFile + " -filter_complex \[0:0\]\[1:0\]concat=n=2:v=0:a=1[a] -map [a] " + outConcatMp3File; } else if (OS.contains("windows")) { command = FFMPEG_PATH + " -i " + inputMp3File + " -i " + inputWavFile + " -filter_complex \"\[0:0\]\[1:0\]concat=n=2:v=0:a=1[a]\" -map \"[a]\" " + outConcatMp3File; } // 合并(视频和音频) // String command = FFMPEG_PATH + " -i " + inputMp4File + " -i " + outConcatMp3File + " -c:v copy -c:a aac -strict experimental " + outMergeMp3AndMp4File; // 合并 // String command = FFMPEG_PATH + " -i " + inputMp3File + " -i " + inputWavFile + " -filter_complex amerge -ac 2 -c:a libmp3lame -q:a 4 " + outMergeMp3File; System.out.println(command); Process process = null; try { process = Runtime.getRuntime().exec(command); } catch (IOException e) { e.printStackTrace(); } if (null == process) { return; } try { process.waitFor(); } catch (InterruptedException e) { e.printStackTrace(); } try (InputStream errorStream = process.getErrorStream(); InputStreamReader inputStreamReader = new InputStreamReader(errorStream); BufferedReader br = new BufferedReader(inputStreamReader)) { String line = null; StringBuffer context = new StringBuffer(); while ((line = br.readLine()) != null) { context.append(line); } System.out.println("error message: " + context); } catch (IOException e) { e.printStackTrace(); } process.destroy(); } } 在我的认知中,完成任务是第一位的,所以按照这个简单处理一下音视频是没有问题的,具体更强大的语法,大家可以自己查询相关文档,也可以参考 https://blog.csdn.net/shshjj/article/details/98185454 这篇文中,其中我个人也在学习中。下面说两个在使用的过程中遇到的问题。 ...

February 29, 2020 · 2 min · 307 words · Bridge Li

日期中用 YYYY 一定会报错吗?

今年春节真是打破了 N 多传统,很多人都是在家连门都没出过,从今天开始也要开始在家远程办公了,因为和小伙伴合作开发一个功能,但是接口目前还没给到,然后记得今年元旦前后,关于 YYYY 报错的问题,突然火了,据说有 N 多程序员被火速召回公司改 bug,所以决定写篇小文章说说这个问题:YYYY 一定会报错吗? 首先需要说明的是:我用的 JDK 版本为:jdk-8u131-macosx-x64,所以具体表现为应该显示:2019-12-31,结果确是:2020-12-31,另外经过我的测试其实不仅 format 的时候报错,parse 的时候同样也会报错,我写了一段示例代码如下: package cn.bridgeli.demo; import org.joda.time.DateTime; import org.junit.Test; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; /** * Created by bridgeli on 2019/02/03. */ public class DateTest { @Test public void testSimpleDateFormat() throws ParseException { SimpleDateFormat simpleDateFormat1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); DateTime dateTime1 = new DateTime(2019, 12, 31, 23, 59, 59); String date1 = simpleDateFormat1.format(dateTime1.toDate()); System.out.println("date1: " + date1); SimpleDateFormat simpleDateFormat2 = new SimpleDateFormat("yyyy-MM-dd"); DateTime dateTime2 = new DateTime(2020, 01, 01, 23, 59, 59); String date2 = simpleDateFormat2.format(dateTime2.toDate()); System.out.println("date2: " + date2); SimpleDateFormat simpleDateFormat3 = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss"); DateTime dateTime3 = new DateTime(2019, 12, 31, 23, 59, 59); String date3 = simpleDateFormat3.format(dateTime3.toDate()); System.out.println("date3: " + date3); SimpleDateFormat simpleDateFormat4 = new SimpleDateFormat("YYYY-MM-dd"); DateTime dateTime4 = new DateTime(2020, 01, 01, 23, 59, 59); String date4 = simpleDateFormat4.format(dateTime4.toDate()); System.out.println("date4: " + date4); SimpleDateFormat simpleDateFormat5 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date date5 = simpleDateFormat5.parse("2019-12-31 23:59:59"); System.out.println("date5: " + date5); SimpleDateFormat simpleDateFormat6 = new SimpleDateFormat("yyyy-MM-dd"); Date date6 = simpleDateFormat6.parse("2020-01-01"); System.out.println("date6: " + date6); SimpleDateFormat simpleDateFormat7 = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss"); Date date7 = simpleDateFormat7.parse("2019-12-31 23:59:59"); System.out.println("date7: " + date7); SimpleDateFormat simpleDateFormat8 = new SimpleDateFormat("YYYY-MM-dd"); Date date8 = simpleDateFormat8.parse("2020-01-01"); System.out.println("date8: " + date8); } } 上面这段代码的输出结果为: ...

February 3, 2020 · 3 min · 541 words · Bridge Li

纠错:Java 内存模型(JMM)

当一个 Java 程序员工作一段时间之后,不可避免的要去了解 JVM,而了解 JVM 的时候,自然就会看到 Java 的内存模型,但是个人看过有太多的人概念不清不楚,有太多的人把 Java 内存结构,记得曾经看过一篇文章,一个同学去面试,面试官问他:简单聊聊 Java 的内存模型,他说完之后,面试官说:你说的不对啊,内存模型应该是堆、栈、常量池、方法区、程序计数器等等,然后他瞬间就不想去这家公司了,这不是让一个半吊子去当面试官吗,自己概念都没搞清楚就去说别人,个人也看过太多文章把这个搞错,所以今天就写了一篇小文章,说说自己对 JMM 的理解,当然我说的可能也不对,也不可能说透。 主内存和工作内存 Java 内存模型规定了所有的变量都存储在主内存,每个线程还有自己的工作内存,线程的工作内存中存储了被该线程使用到的变量的主内存存储拷贝,程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,不能直接读写主内存中的变量,不同的线程之间也无法直接访问对方工作内存的变量,线程间变量值的传递均需要通过主内存来完成,主内存和工作内存之间的交互有 8 个人原子性的操作来实现,具体详细的可以再查资料。 重排序 重排序是编译器和处理器为了优化程序性能,而对指令顺序进行重新排序的一种手段。但 as-if-serial 语义的意思是:不管怎么重排序,单线程程序的执行结果都不能被改变,所以编译器和处理器都不会对存在数据依赖关系的操作做重排序。 happens-before 如果操作 1 happens-before 操作 2,那么第操作1的执行结果将对操作 2 可见,而且操作1的执行顺序排在第操作 2 之前。两个操作之间存在 happens-before 关系,并不意味着一定要按照 happens-before 原则制定的顺序来执行。如果重排序之后的执行结果与按照 happens-before 关系来执行的结果一致,那么这种重排序并不非法。另外 happens-before 具有传递性,由于这些规则的存在,Java 内存模型会保证重排序后的执行,在线程内看起来和串行的效果是一样的,这就是程序的顺序一致性。 对于 volatile 型变量的特殊规则 当一个变量被定义为 volatile 之后,他将具备两种特性,1. 保证此变量对所有线程的可见行,这里的可见行是指当一个线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的,而普通变量做不到这一点。但是这个要多说一点,有些同学看到这可能认为:volatile 变量在各个线程中是一致的,所以基于 volatile 变量的运算在并发下是安全的,其实这句话的论据对,但结论是错的,因为 Java 中的运算并非原子的,volatile 的应用场景在于读多写少的地方:例如修饰一个 boolean 变量作为一个开关。2. 禁止指令重拍优化,这个在写单例的时候,我想大家都已经知道了,不再赘述。 final 被 final 修饰的字段,一旦完成了初始化,其他线程就能看到它,并且它也不会再变了。即只要不可变对象被正确的构建出来(没有发生 this 引用溢出),它就是线程安全的。

January 1, 2020 · 1 min · 64 words · Bridge Li

Apache Commons Codec — 加密与编码

明天就是十一假期了,公司也没多大事,刷知乎,看到有人吐槽曾经的一个合作伙伴连 md5 都写不对,告诉对方写错了,对方顾头不顾腚的修,还是没修对,然后测了一下自己写的想亏写对了,不然又遗留 bug 了,不过看下面评论,有人提到 Apache Commons Codec 里面都已经写好了,看了一下确实,反正也无聊,也写不了大文章,写着玩玩。 先看自己手写的 md5 // 今后大家不要这么写了,太傻了 package cn.bridgeli.demo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; /** * Created by bridgeli on 2019/7/12. */ public class EncryptUtils { private static Logger logger = LoggerFactory.getLogger(EncryptUtils.class); private EncryptUtils() { } public static String getMD5(String content) { if (null == content) { return ""; } MessageDigest messageDigest = null; try { messageDigest = MessageDigest.getInstance("md5"); } catch (NoSuchAlgorithmException e) { logger.error("md5 error", e); } if (null == messageDigest) { return ""; } messageDigest.update(content.getBytes()); byte[] bytes = messageDigest.digest(); StringBuilder stringBuilder = new StringBuilder(); for (byte b : bytes) { String str = Integer.toHexString(b & 0xFF); if (str.length() == 1) { stringBuilder.append("0"); } stringBuilder.append(str); } String result = stringBuilder.toString(); return result; } } 借助别人写好的工具类实现 先引入依赖 ...

September 30, 2019 · 2 min · 272 words · Bridge Li

关于 alibaba fastjson 的两个小知识点

json 转 JavaBean 大小写不敏感 在工作中,我个人经常使用的 json 的工具类是 Google 的 gson,前几天做一个需求的也自然而然的使用这个,但是在和其他部门联调的时候,发现他的属性全是小写,而不是刚开始约定的小驼峰,所以导致他传过来的字符串,我这边转不成 Java bean,在和他讨论的时候,他说他一直就这么写,而且别人使用的时候是没有问题的,然后就找到其他的使用的同学,他竟然说他没在意过这个问题,线上测试了一下,确实没有问题,然后就看一下怎么实现的,然后突然发现,他使用的是 alibaba 的 fastjson 转 Java bean,然后测试了一下,发现 alibaba fastjson 转 Java bean 竟然是大小写不敏感的,特此记录一下,测试代码入校: <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.37</version> </dependency> package cn.bridgeli.demo; import com.alibaba.fastjson.JSON; import org.junit.Test; import java.io.Serializable; import java.util.ArrayList; import java.util.List; public class FastJsonTest { @Test public void testFastJsonParseObject() { String jsonStr = "{\"USERNAME\":\"BridgeLi\"}"; User user = JSON.parseObject(jsonStr, User.class); System.out.println(user.getUsername()); } } class User implements Serializable { private static final long serialVersionUID = 4202834388700617773L; private String username; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } } 我们看到测试的例子中 Java bean 的属性是小写的,json 字符串中的属性是全大写的,但转换一点问题都没有。 ...

August 25, 2019 · 1 min · 184 words · Bridge Li

关于 MySQL 和 MyBatis 易错的几个点

由于某些不可抗拒力原因,自从开博以来断更了一个月,昨天晚上突然发现竟然解封了,今天立即写一篇小文章感谢党感谢政府感谢人民。话说,这一周有一个实习的同学,在写一个小东西的时候,发现一个问题,排序没有生效,刚好之前我也看过另外一个问题,现在算是总结一下。 ORDER BY 不生效 代码大概就是: SELECT * FROM t ORDER BY "id DESC"; 我们其实可以很明显的看出来这个 SQL 有问题,ORDER BY 的后面多了引号,关键是这个 SQL 报错吗?如果 id 是随便写的一个不存在的列报错吗?答案是都不报错,排序也不生效,大家可以测试一些,这有时候就比较坑了,具体为什么会出现这个错误,后面再说,先说第二个。 分页问题 代码如下: /** * 开始页码 */ private String pageSize; /** * 分页量 */ private String offset; <if test="pageSize != null"> <if test="offset != null"> limit #{offset}, #{pageSize} </if> <if test="offset == null"> limit #{pageSize} </if> </if> 这个错误就比较明显了,一般人也不会犯,为什么要单独说一下呢?因为某同事说,分页用 # 会出错,但是我记得我一直用 # 没问题啊,刚好在遇到上面这个问题的时候,顺便测试了一些,还真有错,奇怪啊,其实原因很简单,就是分页的对象的属性有误,我们知道分页的属性,一般都是用 Integer,这个地方却写了 String,真是没事找事啊,一般谁会这么写?个人认为除非脑子有病。下面看第三个问题,也是一个比较诡异的问题。 UPDATE UPDATE t SET username = "BridgeLi" AND passwd = "BridgeLi" WHERE id = 1; 这个 SQL 有问题吗?会报错吗?这个问题有时候真不太看得出来,其实也很明显,SET 后面的各个列应该是 “,” 相连,而这个用的是 AND,有时候脑子一抽,还真有可能写错。但是会报错吗?这个问题还真不好答复,因为这个 SQL 这么写,相当于: ...

July 8, 2019 · 1 min · 186 words · Bridge Li

Redis GeoHash 的一个小示例

上周产品经理提了一个类似于 LBS 的应用,第一时间想到了忘记了之前什么时候看 Redis 的 API,发现 Redis 自 3.2 版本之后,新增了一类关于地理位置相关的 API,于是拿来测试一下,发现特别好用,写一个小例子作为笔记。 首先需要说明的是,由于我们公司的 JDK 的版本是 1.7,所以我采用的 spring-data-redis 的版本是:1.8.20.RELEASE,最新二点几的版本已经不支持 JDK 1.7,而一点几和二点几的版本的 API 有略微的差异(下面会说明,还有一点点我的小感悟),废话不多说,直接看例子: @Override public Long geoAdd(String key, List<Entity> entities) { redisTemplate.delete(key); GeoOperations geoOperations = redisTemplate.opsForGeo(); Map<String, Point> map = new HashMap<>(); Point point = null; for (Entity entity : entities) { point = new Point(entity.getLongitude(), entity.getLatitude()); map.put(gson.toJson(entity), point); } Long add = geoOperations.geoAdd(key, map); return add; } 参数就两个很简单,一个是 key,一个是数据集,我们将在这个集合中找出符合条件的数据,需要说明的是: 第一行我先调用了一个 delete 方法,是将上次放进去的数据删除,因为这个命令是 add,也就是新增,但是 redis 并没有提供直接删除这个 key 的命令(有一个 remove 的方法,但是需要传入删除哪些数据,也就是不能只给一个 key,把这个 key 对应的数据都删除,个人感觉不太好用),当然你也可以在计算后取得相应的数据之后删除,个人感觉都一样,不要忘记清理数据就行,另一个方法就是设置过期时间,也都行; 为什么要清理数据?因为这个 add,不同的情况,放进去的数据应该不同的,如果 entities 已经发生变更,而一直 add,那么数据将会乱掉,所以先把之前的数据删掉再说; 我个人采用的是把数据放到了 Map 中,其中 key 是对象序列化之后的 json 串,目的是为了下面找到对应的数据之后,直接反序列化成对象进行返回,当然也可以采用其他的方案,还有就是 add 还有一些其他的 API,这个大家可以自己看文档,选择合适的就行; 数据放进去之后就是计算了,我们的需求就是算一个人旁边几公里内有多少符合条件的数据,代码如下: ...

May 19, 2019 · 2 min · 323 words · Bridge Li

分享 Guava 的一些常见方法

前几天同事分享了一些关于 Guava 的一起基础用法,我之前没用过,感觉挺好的,所以记一些常见的方法。 一. 基础工具类,字符串相关的 其实这些在 apache commons-lang3,算是重复造轮子吧,简单说一下。 判断字符串是否为空,之前看到很多人自己定义,这些可能是一些老程序员吧,apache commons-lang3,Guava 的如下: boolean nullOrEmpty = Strings.isNullOrEmpty(""); 补全字符串(在前面补全和后面补全) String padStart0 = Strings.padStart("3", 2, &#8216;a&#8217;); System.out.println("padStart0 = " + padStart0); String padStart1 = Strings.padStart("333", 2, &#8216;a&#8217;); System.out.println("padStart1 = " + padStart1); 拆分和合并字符串 List<String> list = Splitter.on(",").splitToList("Denny,BridgeLi,CCC"); System.out.println("list = " + list); String join = Joiner.on(",").join(list); System.out.println("join = " + join); 对象相等判断和 ToStringHelper 类 boolean equal = Objects.equal("", ""); MoreObjects.toStringHelper(); 二. 集合类相关的,这些个人感觉还是非常常用的 ...

April 30, 2019 · 2 min · 366 words · Bridge Li

关于 CAP 理论 和 BASE 理论

一、CAP 理论 CAP 理论是分布式计算领域公认的一个定理,是分布式架构师必须掌握的理论,目前网上关于这块的资料也很多,各种说法,其实 CAP 理论自身也是一个不断发展的过程,相比之下比较准确的说法应该是:在一个分布式系统(指互相连接并共享数据的节点的集合)中,当涉及读写操作时,只能保证一致性(Consistence)、可用性(Availability)、分区容错性(Partition Tolerance)三者中的两个,另外一个必须被牺牲。也就是说必须是相关连接并且共享数据的分布式系统才是我们讨论 CAP 的基础,另外就是 CAP 理论关注的是对数据的读写操作,而不是分布式系统的所有功能。明白了这些之后,下面对 CAP 逐一讲解: 一致性(Consistency) 很多资料都说,对一致性的讲解时所有节点在同一时刻都能看到相同的数据,其实更准确的应该是:对某个客户端来说,读操作保证能够返回最新的写操作结果,是否数据一定一直呢?还真不一定,对于系统执行事务来说,在事务执行过程中,系统其实处于一个不一致的状态,不同的节点的数据并不完全一致,因为事务在执行过程中,client 是无法读取到未提交的数据的,只有等到事务提交后,client 才能读取到事务写入的数据,而如果事务失败则会进行回滚,client 也不会读取到事务中间写入的数据。 可用性(Availability) 一般的解释是,每个请求都能得到成功或者失败的响应,更准确的是,非故障的节点在合理的时间内返回合理的响应(不是错误和超时的相应),其实什么是成功和失败?超时报错算不算失败的响应?所以应该是合理的响应,另外故障节点肯定是不能返回结果了。 分区容错性(Partition Tolerance) 一般说法是,出现消息丢失或者分区错误时系统能够继续运行,其实更准确的是,当出现网络分区后,系统能够继续“履行职责”。因为关于运行,只要不宕机就可以说在运行。 综上,其实 CAP 理论也是一个自身不断发展的历程,虽然以前的理论也能说明问题,但是仔细深究起来发现有些问题,下面会有说明。 CAP 应用 CAP 理论开明宗义就说我们必须放弃一个,但是其实仔细想想我们可以放弃 P 吗?因为网络本身无法做到 100% 可靠,有可能出故障,所以分区是一个必然的现象。如果我们选择了 CA 而放弃了 P,那么当发生分区现象时,为了保证 C,系统需要禁止写入,当有写入请求时,系统返回 error(例如,当前系统不允许写入),这又和 A 冲突了,因此,分布式系统理论上不可能选择 CA 架构,只能选择 CP 或者 AP 结果,但是需要说明的是,按照通常解释,是可以 CA 的,因为通常的解释 A 是返回失败或者成功的结果,而不允许写入,其实就是失败的结果,满足 A 的定义 CP 为了保证一致性,当发生分区现象后,N1 节点上的数据已经更新到 y,但由于 N1 和 N2 之间的复制通道中断,数据 y 无法同步到 N2,N2 节点上的数据还是 x。这时客户端 C 访问 N2 时,N2 需要返回 Error,提示客户端 C“系统现在发生了错误”,这种处理方式违背了 A 的要求,因此 CAP 三者只能满足 CP ...

March 31, 2019 · 1 min · 149 words · Bridge Li