Archive for the ‘Code杂谈’ category

对缓存更新的思考

August 5th, 2020

缓存更新方案是通过对更新缓存和更新数据库这两个操作的设计,来实现数据的最终一致性,避免出现业务问题。

先来看一下什么时候创建缓存,前端请求的读操作先从缓存中查询数据,如果没有命中数据,则查询数据库,从数据库查询成功后,返回结果,同时更新缓存,方便下次操作。

在数据不发生变更的情况下,这种方式没有问题,如果数据发生了更新操作,就必须要考虑如何操作缓存,保证一致性。

先更新数据库,再更新缓存
先来看第一种方式,在写操作中,先更新数据库,更新成功后,再更新缓存。这种方式最容易想到,但是问题也很明显,数据库更新成功以后,由于缓存和数据库是分布式的,更新缓存可能会失败,就会出现上面例子中的问题,数据库是新的,但缓存中数据是旧的,出现不一致的情况。

先删缓存,再更新数据库
这种方案是在数据更新时,首先删除缓存,再更新数据库,这样可以在一定程度上避免数据不一致的情况。

现在考虑一个并发场景,假如某次的更新操作,更新了商品详情 A 的价格,线程 A 进行更新时失效了缓存数据,线程 B 此时发起一次查询,发现缓存为空,于是查询数据库并更新缓存,然后线程 A 更新数据库为新的价格。

在这种并发操作下,缓存的数据仍然是旧的,出现业务不一致。

先更新数据库,再删缓存
这个是经典的缓存 + 数据库读写的模式,有些资料称它为 Cache Aside 方案。具体操作是这样的:读的时候,先读缓存,缓存没有的话,那么就读数据库,然后取出数据后放入缓存,同时返回响应,更新的时候,先更新数据库,数据库更新成功之后再删除缓存。

为什么说这种方式经典呢?

在 Cache Aside 方案中,调整了数据库更新和缓存失效的顺序,先更新数据库,再失效缓存。

目前大部分业务场景中都应用了读写分离,如果先删除缓存,在读写并发时,可能出现数据不一致。考虑这种情况:

线程 A 删除缓存,然后更新数据库主库;

线程 B 读取缓存,没有读到,查询从库,并且设置缓存为从库数据;

主库和从库同步。

在这种情况下,缓存里的数据就是旧的,所以建议先更新数据库,再失效缓存。当然,在 Cache Aside 方案中,也存在删除缓存失败的可能,因为缓存删除操作比较轻量级,可以通过多次重试等来解决,你也可以考虑下有没有其他的方案来保证。

对缓存更新的思考

为什么删除而不是更新缓存
现在思考一个问题,为什么是删除缓存,而不是更新缓存呢?删除一个数据,相比更新一个数据更加轻量级,出问题的概率更小。

在实际业务中,缓存的数据可能不是直接来自数据库表,也许来自多张底层数据表的聚合。比如上面提到的商品详情信息,在底层可能会关联商品表、价格表、库存表等,如果更新了一个价格字段,那么就要更新整个数据库,还要关联的去查询和汇总各个周边业务系统的数据,这个操作会非常耗时。

从另外一个角度,不是所有的缓存数据都是频繁访问的,更新后的缓存可能会长时间不被访问,所以说,从计算资源和整体性能的考虑,更新的时候删除缓存,等到下次查询命中再填充缓存,是一个更好的方案。

系统设计中有一个思想叫 Lazy Loading,适用于那些加载代价大的操作,删除缓存而不是更新缓存,就是懒加载思想的一个应用。

多级缓存如何更新
再看一个实际应用中的问题,多级缓存如何更新?

多级缓存是系统中一个常用的设计,服务端缓存分为应用内缓存和外部缓存,比如在电商的商品信息展示中,可能会有多级缓存协同。那么多级缓存之间如何同步数据呢?

常见的方案是通过消息队列通知的方式,也就是在数据库更新后,通过事务性消息队列加监听的方式,失效对应的缓存。多级缓存比较难保证数据一致性,通常用在对数据一致性不敏感的业务中,比如新闻资讯类、电商的用户评论模块等。上面的内容是几种常用的缓存和数据库的双写一致性方案,大家在开发中肯定应用过设计模式,这些缓存应用套路和设计模式一样,是前人在大量工程开发中的总结,是一个通用的解决范式。在具体业务中,还是需要有针对性地进行设计,比如通过给数据添加版本号,或者通过时间戳 + 业务主键的方式,控制缓存的数据版本实现最终一致性。

 

设计微信朋友圈的社交系统

July 19th, 2020

你自己可以发朋友圈,刷朋友,看到你自己发的朋友圈以及你的好友发的朋友圈,你可以对朋友圈进行点赞,进行评论,你可以去设置权限,你不看某些人的朋友圈,不让某些人看你的朋友圈,拉黑或者删除某个好友再也不用看到他的朋友圈了,首先你发送朋友圈的时候,一般是9张图片配合一些文字,组成了一条朋友前,文字还好说,但是图片就会稍微有点大了,也可以是一个短视频配合一些文字,点击发送,假设你要同步发送,可能会导致你点击发送按钮之后,弹出一个旋转框,告诉你发送中,持续好几秒种中,用户体验是比较差的。

有一个好一点的办法,可以把这些数据在客户端本地暂存一下,然后直接让你发送成功返回,走一个异步发送状态,然后立马让你自己在刷朋友圈的时候,可以把你客户端本地的刚发的朋友圈加载出来看到仅仅是这样,就会变成,发朋友圈成了你自己的自娱自乐,因为你的朋友圈并没有发送出去让你的好朋友看到,可以走一个异步的模式,把你的朋友圈里的图片或者视频+文字,花费几秒钟的时间传送到你的后台服务器上去存储之后,你的朋友就可以从后台服务器上加载你的朋友圈里的图片和视频,可以看到了你的那些视频和图片,是不是可以就都直接就近上传到CDN(content delivery network),不是直接到朋友圈系统的后台,这样速度是很快的;

接着就是发送请求到朋友圈后端系统,请求包括图片的地址,你配的文字,发朋友圈的时候可以选择开放给谁看,这些数据写入到朋友圈发布表里去然后需要在相册表里写入索引数据,里面存放的是对你的发布表里的数据引用,这样你以后浏览相册的时候,都是根据相册里的索引数据到发布表里找实际对应的数据的接着就走一个离线批处理,通过批处理程序把这条朋友圈写入到你所有好友的时间线表里去,你好友的时间线表里就是存放了他刷朋友圈的时候,可以按照时间线刷到的所有好友的朋友圈你有3个好朋友,每个好朋友都在时间线表里有一个朋友圈的时间线,按照时间顺序排列了他可以查看的所有朋友圈,包括了他自己发的朋友圈以及他的好友发的朋友圈允许他看的那些,都会在这里然后你的好朋友刷朋友圈的时候,就会知道自己的时间线表里有个新的变化,就是有好友发了朋友圈,此时就会提示你一个红色的圆点,你就开始刷,刷的时候就根据图片url地址去cdn拉取。

 

权限控制

这条朋友圈的权限到了后台之后,会有一个离线批处理的程序跑起来,对最近发的一波朋友圈都找他们的朋友圈的权限的设置看一下,此时就会对你允许看到的好友,此时就在他们的时间线里插入这条朋友圈数据,那么这样的话,只有你允许的好友的时间线里才有你这条朋友圈

 

比如说王五发的朋友圈16931可以允许张三和李四看到,设置了一个标签组,标签名称是老铁三人组,里面就正好有张三和李四

 

张三       发表朋友圈的时间戳           朋友圈16931        王五

张三       发表朋友圈的时间戳           朋友圈16384        李四

李四       发表朋友圈的时间戳           朋友圈16931        王五

 

在redis里可以设置张三的朋友圈是有变动的一个状态,在上次拉取朋友圈的时间点之后的一些朋友圈都从时间线表里拉取出来,刷朋友圈的时候,如果说你的网速要是不太好的话,你会发现这样一个场景

就是你最新的一些朋友发的朋友圈是显示出来了,但是视频和图片都是一片灰色,仅仅能看到他的文字和其他的一些东西,比如说点赞之类的,图片和视频死活看不到,都是一片灰色,反正我自己网速不好的时候经常看到这样的情况

假设王五之前发了一条朋友圈,设置李四可以看到的,李四之前确实是看到了这条朋友圈的,但是有个问题,王五后来跟李四吵了一架,关系变得非常的不好,王五就对李四设置了一个朋友圈的权限,就是自己的朋友圈不允许李四看到,甚至可能会直接拉黑/删除李四这个好友,这个就够狠了

你设置自己的朋友圈对所有朋友都是仅仅三天之内可见

就是说你跟李四之间的朋友圈的权限总设置或者是朋友之间的关系,有了变化,或者是你的自己的朋友圈对外展示的总权限有了变化,此时每次如果有变动,那么这些设置,包括你对每个朋友的朋友圈权限的设置,跟朋友的关系,自己的朋友圈的总权限,这些设置都会统统的缓存起来包括缓存在你自己的客户端本地,也可以缓存在你的朋友的客户端本地。

但是你可能随时会拉黑、删除某个人,或者是突然设置对那个人朋友圈不可见,或者是突然你自己设置了朋友圈三天可见什么的,所以你设置的这些东西,都会被缓存起来,每次你好友刷朋友圈,查看自己的时间线表的时候,都会检查你的某条朋友圈根据你的一些行为,是否还对他可见。

李四会关注王五的各种朋友圈权限和朋友关系的一个变化,一旦说有变化了,可以缓存到自己的本地,下次在客户端里再次刷新朋友圈的时候,客户端对于王五的朋友圈会和王五的各种权限设置结合起来判断一下李四能否看到王五的这条朋友圈

点赞系统

我看到了你的朋友圈,此时我就可以对你的朋友圈去进行一个点赞,也可以取消点赞,假设要设计成支撑高并发的点赞系统,应该如何设计?

朋友圈的点赞和评论,是独立的数据,其实比如点赞,都是可以基于redis来做的,每个朋友圈里对应一个set数据结构,里面放谁给你点赞了,这样每条朋友圈的点赞人(smembers)和点赞数量(scard)直接从redis出就可以了。

评论也是可以存表里的,都是以朋友圈为粒度来存储。

那么刷朋友圈的时候,比如说你好友和你,另外一个好友都是好友,此时你好友刷到了你的朋友圈,就可以把另外一个好友对你的点赞和评论都拉出来,展示在客户端下面就可以了,这个展示过程可以是动态的。

你是王五,你的朋友圈被张三点赞了,李四跟你们也是好朋友,此时李四刷朋友圈看到了王五发的这条朋友圈,此时你可以在后台,对这条朋友圈的set用张三做一个sismember操作,就是判断一下你们俩的所有共同好友,有哪些人对这条朋友圈点赞了。

此时就可以看出来这条朋友圈被你们的共同好友多少人点赞了,哪些人点赞了。

比如你另外一个好友是否对这条朋友圈点赞了,直接sismember就可以判断出来,这样整个你基于redis,他都是非常高性能的。

正则总结

December 20th, 2019

如果需要匹配的字符串含有特殊字符,那就需要用 \转义。比如 a&b,在用正则表达式匹配时,需要使用 a\&b,又由于在 Java 字符串中,\ 也是特殊字符,它也需要转义,所以 a\&b 对应的 Java 字符串是 a\\&b,它是用来匹配 a&b 的。

System.out.println("a&b".matches("a\\&b")); // 输出为 true

\d\d 就能匹配两个数字,\d\d\d 能匹配三个数字,需要匹配几个数字就写几次就行了。

System.out.println("1".matches("\\d\\d")); // 输出为 false
System.out.println("11".matches("\\d\\d")); // 输出为 true
System.out.println("111".matches("\\d\\d")); // 输出为 false

在 \d 后面打上花括号 {},{n} 表示匹配 n 次。\d{10000} 就表示匹配 10000 个数字。如果要匹配 n ~ m 次,用 {n,m} 即可,如果要匹配至少 n 次,用 {n,} 即可。需要注意 , 后不能有空格。

System.out.println("1".matches("\\d{1,2}")); // 输出为 true
System.out.println("12".matches("\\d{1,2}")); // 输出为 true
System.out.println("123".matches("\\d{1,2}")); // 输出为 false
System.out.println("123".matches("\\d{2,}")); // 输出为 true

正则的基础规则中,除了 \d,还有 \w和\s,w 是 word 的简写,表示匹配一个常用字符,包括字母、数字、下划线。s 是 space 的简写,表示匹配一个空格,包括三种:空格键打出来的空格/Tab 键打出来的空格/回车键打出来的空格。

System.out.println("LeetCode_666".matches("\\w{12}")); // 输出为 true
System.out.println("\t \n".matches("\\s{3}")); // 输出为 true
System.out.println("Leet\tCode 666".matches("\\w{4}\\s\\w{4}\\s\\d{3}")); // 输出为 true

将字母换成大写,就表示相反的意思。用 \d 你可以匹配一个数字,\D 则表示匹配一个非数字。类似地,\W 可以匹配 \w 不能匹配的字符,\S 可以匹配 \s 不能匹配的字符。

System.out.println("a".matches("\\d")); // 输出为 false
System.out.println("1".matches("\\d")); // 输出为 true
System.out.println("a".matches("\\D")); // 输出为 true
System.out.println("1".matches("\\D")); // 输出为 false

我们对某些位置的字符没有要求,仅需要占个位置即可。这时候我们就可以用 . 字符。我们对匹配的次数没有要求,匹配任意次均可,这时,我们就可以用 * 字符。出现了 0 次,* 是指 可以匹配任意次,包括 0 次。也就是说,* 等价于 {0,}

System.out.println("1".matches("\\d*")); // 输出为 true
System.out.println("123".matches("\\d*")); // 输出为 true
System.out.println("".matches("\\d*")); // 输出为 true

可以用 + 匹配,+ 表示 至少匹配一次。它等价于 {1,}

System.out.println("1".matches("\\d+")); // 输出为 true
System.out.println("123".matches("\\d+")); // 输出为 true
System.out.println("".matches("\\d+")); // 输出为 false

如果某个字符要么匹配 0 次,要么匹配 1 次,我们就可以用 ? 匹配。它等价于 {0,1}

如果我们规定电话号码不能以 0 开头,正则匹配规则是 [123456789]\d{10}。

System.out.println("1".matches("[1-9a-gU-Z]")); // 输出为 true
System.out.println("b".matches("[1-9a-gU-Z]")); // 输出为 true
System.out.println("X".matches("[1-9a-gU-Z]")); // 输出为 true
System.out.println("A".matches("[1-9a-gU-Z]")); // 输出为 false

考虑一个实际需求,有许许多多以下格式的字符串,你需要用正则表达式匹配出其姓名和年龄。
Name:Aurora Age:18
其中还夹杂着一些无关紧要的数据
Name:Bob Age:20
错误的数据有着各种各样错误的格式
Name:Cassin Age:22

观察字符串的规则,只需要用 Name:\w+\s*Age:\d{1,3} 就能匹配了。

System.out.println("Name:Aurora   Age:18".matches("Name:\\w+\\s*Age:\\d{1,3}")); // 输出为 true
System.out.println("其中还夹杂着一些无关紧要的数据".matches("Name:\\w+\\s*Age:\\d{1,3}")); // 输出为 false
System.out.println("Name:Bob      Age:20".matches("Name:\\w+\\s*Age:\\d{1,3}")); // 输出为 true
System.out.println("错误的数据有着各种各样错误的格式".matches("Name:\\w+\\s*Age:\\d{1,3}")); // 输出为 false
System.out.println("Name:Cassin   Age:22".matches("Name:\\w+\\s*Age:\\d{1,3}")); // 输出为 true

Pattern pattern = Pattern.compile("Name:(\\w+)\\s*Age:(\\d{1,3})");
Matcher matcher = pattern.matcher("Name:Aurora   Age:18");
if(matcher.matches()) {
    String group1 = matcher.group(1);
    String group2 = matcher.group(2);
    System.out.println(group1);   // 输出为 Aurora
    System.out.println(group2);   // 输出为 18
}

只要用 () 将需要取值的地方括起来,传给 Pattern 对象,再用 Pattern 对象匹配后获得的 Matcher 对象来取值就行了。每个匹配的值将会按照顺序保存在 Matcher 对象的 group 中。用 () 把 \\w+ 和 \\d{1,3} 分别括起来了,判断 Pattern 对象与字符串是否匹配的方法是 Matcher.matches(),如果匹配成功,这个函数将返回 true,如果匹配失败,则返回 false。

group(0) 被用来保存整个匹配的字符串了。

考虑一个实际场景:你有一个让用户输入标签的输入框,用户可以输入多个标签。可是你并没有提示用户,标签之前用什么间隔符号隔开。
二分,回溯,递归,分治
搜索;查找;旋转;遍历
数论 图论 逻辑 概率

System.out.println(Arrays.toString("二分,回溯,递归,分治".split("[,;\\s]+")));
System.out.println(Arrays.toString("搜索;查找;旋转;遍历".split("[,;\\s]+")));
System.out.println(Arrays.toString("数论 图论 逻辑 概率".split("[,;\\s]+")));

System.out.println("二分,回溯,递归,分治".replaceAll("[,;\\s]+", ";"));
System.out.println("搜索;查找;旋转;遍历".replaceAll("[,;\\s]+", ";"));
System.out.println("数论 图论 逻辑 概率".replaceAll("[,;\\s]+", ";"));

在 replaceAll 的第二个参数中,我们可以通过 $1,$2,…来反向引用匹配到的子串。只要将需要引用的部分用 () 括起来就可以了。

System.out.println("二分,回溯,递归,分治".replaceAll("([,;\\s]+)", "---$1---"));
System.out.println("搜索;查找;旋转;遍历".replaceAll("([,;\\s]+)", "---$1---"));
System.out.println("数论 图论 逻辑 概率".replaceAll("([,;\\s]+)", "---$1---"));

输出为:

二分---,---回溯---,---递归---,---分治
搜索---;---查找---;---旋转---;---遍历
数论--- ---图论--- ---逻辑--- ---概率

贪婪匹配和贪心算法原理是一致的。与之对应的匹配方式叫做 非贪婪匹配,非贪婪匹配 会在能匹配目标字符串的前提下,尽可能少的向后匹配。
在需要非贪婪匹配的正则表达式后面加个 ? 即可表示非贪婪匹配。

Pattern pattern = Pattern.compile("(\\w+?)(e*)");
Matcher matcher = pattern.matcher("LeetCode");
if (matcher.matches()) {
    String group1 = matcher.group(1);
    String group2 = matcher.group(2);
    System.out.println("group1 = " + group1 + ", length = " + group1.length());
    System.out.println("group2 = " + group2 + ", length = " + group2.length());
}

str.replaceAll(“[\\.。]+”, “”) 可以匹配所有的. 和 。

消除str.replaceAll(“[^0-9a-zA-Z]”, “”)消除所有的非大小写字母和数字的非法字符。

ACID/CAP/BASE/二段提交/三段提交/TCC

November 6th, 2019

关系型数据库完全满足ACID的特性。

ACID指的是:A: Atomicity 原子性 C: Consistency 一致性 I: Isolation 隔离性 D: Durability 持久性

具有ACID的特性的数据库支持强一致性,强一致性代表数据库本身不会出现不一致,每个事务是原子的,或者成功或者失败,事物间是隔离的,互相完全不影响,而且最终状态是持久落盘的,因此,数据库会从一个明确的状态到另外一个明确的状态,中间的临时状态是不会出现的,如果出现也会及时的自动的修复,因此是强一致的。

如果你在为交易相关系统做技术选型,交易的存储应该只考虑关系型数据库,对于核心系统,如果需要较好的性能,可以考虑使用更强悍的硬件,这种向上扩展(升级硬件)虽然成本较高,但是是最简单粗暴有效的方式,另外,Nosql完全不适合交易场景,Nosql主要用来做数据分析、ETL、报表、数据挖掘、推荐、日志处理等非交易场景。

CAP(帽子理论)

C:Consistency,一致性, 数据一致更新,所有数据变动都是同步的

A:Availability,可用性, 好的响应性能,完全的可用性指的是在任何故障模型下,服务都会在有限的时间处理响应

P:Partition tolerance,分区容错性,可靠性

帽子理论证明,任何分布式系统只可同时满足二点,没法三者兼顾。关系型数据库由于关系型数据库是单节点的,因此,不具有分区容错性,但是具有一致性和可用性,而分布式的服务化系统都需要满足分区容错性,那么我们必须在一致性和可用性中进行权衡,具体表现在服务化系统处理的异常请求在某一个时间段内可能是不完全的,但是经过自动的或者手工的补偿后,达到了最终的一致性。

BASE模型与ACID模型截然不同,满足CAP理论,通过牺牲强一致性,获得可用性。

一般应用在服务化系统的应用层或者大数据处理系统,通过达到最终一致性来尽量满足业务的绝大部分需求。

BASE模型包含个三个元素:

1.BA:Basically Available,基本可用

2.S:Soft State,软状态,状态可以有一段时间不同步

3.E:Eventually Consistent,最终一致,最终数据是一致的就可以了,而不是时时保持强一致

分布式一致性协议

两阶段如下:

1.准备阶段:协调者向参与者发起指令,参与者评估自己的状态,如果参与者评估指令可以完成,参与者会写redo或者undo日志(这也是前面提起的Write-Ahead Log的一种),然后锁定资源,执行操作,但是并不提交

2.提交阶段:如果每个参与者明确返回准备成功,也就是预留资源和执行操作成功,协调者向参与者发起提交指令,参与者提交资源变更的事务,释放锁定的资源;如果任何一个参与者明确返回准备失败,也就是预留资源或者执行操作失败,协调者向参与者发起中止指令,参与者取消已经变更的事务,执行undo日志,释放锁定的资源

两阶段提交协议

我们看到两阶段提交协议在准备阶段锁定资源,是一个重量级的操作,并能保证强一致性,但是实现起来复杂、成本较高,不够灵活,更重要的是它有如下致命的问题:

1.阻塞:从上面的描述来看,对于任何一次指令必须收到明确的响应,才会继续做下一步,否则处于阻塞状态,占用的资源被一直锁定,不会被释放

2.单点故障:如果协调者宕机,参与者没有了协调者指挥,会一直阻塞,尽管可以通过选举新的协调者替代原有协调者,但是如果之前协调者在发送一个提交指令后宕机,而提交指令仅仅被一个参与者接受,并且参与者接收后也宕机,新上任的协调者无法处理这种情况

3.脑裂:协调者发送提交指令,有的参与者接收到执行了事务,有的参与者没有接收到事务,就没有执行事务,多个参与者之间是不一致的

上面所有的这些问题,都是需要人工干预处理,没有自动化的解决方案,因此两阶段提交协议在正常情况下能保证系统的强一致性,但是在出现异常情况下,当前处理的操作处于错误状态,需要管理员人工干预解决,因此可用性不够好,这也符合CAP协议的一致性和可用性不能兼得的原理。

三阶段提交协议

三阶段提交协议是两阶段提交协议的改进版本。它通过超时机制解决了阻塞的问题,并且把两个阶段增加为三个阶段:

1.询问阶段:协调者询问参与者是否可以完成指令,协调者只需要回答是还是不是,而不需要做真正的操作,这个阶段超时导致中止

2.准备阶段:如果在询问阶段所有的参与者都返回可以执行操作,协调者向参与者发送预执行请求,然后参与者写redo和undo日志,执行操作,但是不提交操作;如果在询问阶段任何参与者返回不能执行操作的结果,则协调者向参与者发送中止请求,这里的逻辑与两阶段提交协议的的准备阶段是相似的,这个阶段超时导致成功

3.提交阶段:如果每个参与者在准备阶段返回准备成功,也就是预留资源和执行操作成功,协调者向参与者发起提交指令,参与者提交资源变更的事务,释放锁定的资源;如果任何一个参与者返回准备失败,也就是预留资源或者执行操作失败,协调者向参与者发起中止指令,参与者取消已经变更的事务,执行undo日志,释放锁定的资源,这里的逻辑与两阶段提交协议的提交阶段一致

三阶段提交协议

然而,这里与两阶段提交协议有两个主要的不同:

1.增加了一个询问阶段,询问阶段可以确保尽可能早的发现无法执行操作而需要中止的行为,但是它并不能发现所有的这种行为,只会减少这种情况的发生

2.在准备阶段以后,协调者和参与者执行的任务中都增加了超时,一旦超时,协调者和参与者都继续提交事务,默认为成功,这也是根据概率统计上超时后默认成功的正确性最大

三阶段提交协议与两阶段提交协议相比,具有如上的优点,但是一旦发生超时,系统仍然会发生不一致,只不过这种情况很少见罢了,好处就是至少不会阻塞和永远锁定资源。

TCC

无论两阶段还是三阶段方案中都包含多个参与者、多个阶段实现一个事务,实现复杂,性能也是一个很大的问题,因此,在互联网高并发系统中,鲜有使用两阶段提交和三阶段提交协议的场景。

阿里巴巴提出了新的TCC协议,TCC协议将一个任务拆分成Try、Confirm、Cancel,正常的流程会先执行Try,如果执行没有问题,再执行Confirm,如果执行过程中出了问题,则执行操作的逆操Cancel,从正常的流程上讲,这仍然是一个两阶段的提交协议,但是,在执行出现问题的时候,有一定的自我修复能力,如果任何一个参与者出现了问题,协调者通过执行操作的逆操作来取消之前的操作,达到最终的一致状态。

可以看出,从时序上,如果遇到极端情况下TCC会有很多问题的,例如,如果在Cancel的时候一些参与者收到指令,而一些参与者没有收到指令,整个系统仍然是不一致的,这种复杂的情况,系统首先会通过补偿的方式,尝试自动修复的,如果系统无法修复,必须由人工参与解决。

从TCC的逻辑上看,可以说TCC是简化版的三阶段提交协议,解决了两阶段提交协议的阻塞问题,但是没有解决极端情况下会出现不一致和脑裂的问题。然而,TCC通过自动化补偿手段,会把需要人工处理的不一致情况降到到最少,也是一种非常有用的解决方案,根据线人,阿里在内部的一些中间件上实现了TCC模式。

我们给出一个使用TCC的实际案例,在秒杀的场景,用户发起下单请求,应用层先查询库存,确认商品库存还有余量,则锁定库存,此时订单状态为待支付,然后指引用户去支付,由于某种原因用户支付失败,或者支付超时,系统会自动将锁定的库存解锁供其他用户秒杀。

总结一下,两阶段提交协议、三阶段提交协议、TCC协议都能保证分布式事务的一致性,他们保证的分布式系统的一致性从强到弱,TCC达到的目标是最终一致性,其中任何一种方法都可以不同程度的解决案例2:转账、案例3:下订单和扣库存的问题,只是实现的一致性的级别不一样而已,对于案例4:同步超时可以通过TCC的理念解决,如果同步调用超时,调用方可以使用fastfail策略,返回调用方的使用方失败的结果,同时调用服务的逆向cancel操作,保证服务的最终一致性。

分布式一致性的技术包括:paxos算法、raft算法、zab算法、nwr算法、一致性哈希等。

参考:

案例2:转账
转账是经典的不一致案例,设想一下银行为你处理一笔转账,扣减你账户上的余额,然后增加别人账户的余额;如果扣减你的账户余额成功,增加别人账户余额失败,那么你就会损失这笔资金。反过来,如果扣减你的账户余额失败,增加别人账户余额成功,那么银行就会损失这笔资金,银行需要赔付。对于资金处理系统来说,上面任何一种场景都是不允许发生的,一旦发生就会有资金损失,后果是不堪设想的,严重情况会让一个公司瞬间倒闭,可参考案例。

案例3:下订单和扣库存
电商系统中也有一个经典的案例,下订单和扣库存如何保持一致,如果先下订单,扣库存失败,那么将会导致超卖;如果下订单没有成功,扣库存成功,那么会导致少卖。两种情况都会导致运营成本的增加,严重情况下需要赔付。

案例4:同步超时
服务化的系统间调用常常因为网络问题导致系统间调用超时,即使是网络很好的机房,在亿次流量的基数下,同步调用超时也是家常便饭。系统A同步调用系统B超时,系统A可以明确得到超时反馈,但是无法确定系统B是否已经完成了预定的功能或者没有完成预定的功能。于是,系统A就迷茫了,不知道应该继续做什么,如何反馈给使用方。(曾经的一个B2B产品的客户要求接口超时重新通知他们,这个在技术上是难以实现的,因为服务器本身可能并不知道自己超时,可能会继续正常的返回数据,只是客户端并没有接受到结果罢了,因此这不是一个合理的解决方案)。

https://blog.csdn.net/qq_31854907/article/details/92796788

对称加密和非对称加密

October 7th, 2019

对称加密和非对称加密的概念是两种非常重要的加密方式,二者各有应用的领域,今天看完一片博文系统性的把这个弄明白了。

在对称加密算法中,加密使用的密钥和解密使用的密钥是相同的。也就是说,加密和解密都是使用的同一个密钥(举例密钥A,甲方用密钥A加密报文,乙方用密钥A解密报文)。显而易见对称加密算法要保证安全性的话,密钥A非常重要,密钥A丢失的话,黑客就可以监听甲方和乙方之间的通信。

在非对称加密中,会同时存在两个密钥(公钥&私钥)然后可以使用公钥加密->私钥解密(或者私钥解密->公钥加密)。 加密使用的密钥和解密使用的密钥是不相同的,因此它是一个非对称加密算法。注意:公钥和私钥只是名义上这么说,主要表达的意思是公钥人人都可以获取,私钥只有自己留存。RSA就是一种非对称加密算法,这一对公钥、私钥都可以用来加密和解密,并且一方加密的内容可以由并且只能由对方进行解密。

什么时候用公钥加密->私钥解密,还是私钥解密->公钥加密主要依靠使用场景:

1.私钥加密公钥解密,能证明“私钥拥有者” 的唯一身份,用于签名。

2. 公钥加密私钥解密,确保发送的信息,只有有“私钥拥有者” 能够接收(如果用私钥加密,传递数据,则会被公钥持有者(可能有很多持有者) 解密,失去对信息的保护)

但是仅有RSA还无法完全保证通信的安全,因此一般通信流程是先使用非对称加密验证双方身份,然后采用对称加密进行数据通信。但是!谁都可以生成公钥私钥,如果黑客向“客户”发送他自己的私钥就可以冒充“服务器”了!因此对于验证身份采用证书方式是现在通用方式。申请证书后,公司会得到这个这个证书和私钥,公钥嵌入在证书中!私钥只能由公司自己知道。

一个证书包含下面的具体内容:

  • 证书的颁发者
  • 证书的有效期
  • 证书的使用者
  • 公钥(这个公司证书的公钥)
  • 证书所有者(Subject)
  • 签名所使用的算法
  • 指纹以及指纹算法(机构的证书)

指纹算法主要用来保证这个证书的完整性,机构会使用自己加密算法和私钥加密证书:生成方式就是”SecureTrust CA”这个证书机构利用签名算法(Signature algorithm)使用私钥加密hash(证书)后和证书放在一起,以保证证书完整性。用户只需要使用机构的公钥解密证书,得到hash’(证书)并和该证书hash对比得出是否被篡改。

如果证书没有篡改,那么使用指纹算法计算这个证书的指纹,将这个计算的指纹与证书中的指纹对比,如果一致,说明这个的证书肯定没有被修改过并且证书是由这个证书机构发布的,证书中的公钥肯定是这个证书的,客户然后就可以放心的使用这个公钥和服务器进行通信。

下面来说HTTPS通信模式:

step1: “客户”向服务端发送一个通信请求

“客户”->“服务器”:你好

step2: “服务器”向客户发送自己的数字证书。证书中有一个公钥用来加密信息,私钥由“服务器”持有

“服务器”->“客户”:你好,我是服务器,这里是我的数字证书

step3: “客户”收到“服务器”的证书后,它会去验证这个数字证书到底是不是“服务器”的,数字证书有没有什么问题,数字证书如果检查没有问题,就说明数字证书中的公钥确实是“服务器”的。检查数字证书后,“客户”会发送一个随机的字符串给“服务器”,“服务器”用私钥去加密然后返回给“客户”,“客户”用公钥解密这个返回结果,如果解密结果与之前生成的随机字符串一致,那说明对方确实是私钥的持有者,或者说对方确实是“服务器”。

“客户”->“服务器”:向我证明你就是服务器,这是一个随机字符串 //前面的例子中为了方便解释,用的是“你好”等内容,实际情况下一般是随机生成的一个字符串。

“服务器”->“客户”:{一个随机字符串}[私钥|RSA]

step4: 验证“服务器”的身份后,“客户”生成一个对称加密算法和密钥,用于后面的通信的加密和解密。这个对称加密算法和密钥,“客户”会用公钥加密后发送给“服务器”,别人截获了也没用,因为只有“服务器”手中有可以解密的私钥。这样,后面“服务器”和“客户”就都可以用对称加密算法来加密和解密通信内容了。

“服务器”->“客户”:{OK,已经收到你发来的对称加密算法和密钥!有什么可以帮到你的?}[密钥|对称加密算法]

“客户”->“服务器”:{我的帐号是aaa,密码是123,把我的余额的信息发给我看看}[密钥|对称加密算法]

“服务器”->“客户”:{你好,你的余额是100元}[密钥|对称加密算法]

…… //继续其它的通信

 

http://www.cnblogs.com/JeffreySun/archive/2010/06/24/1627247.html