All posts by dotte

应用程序开发: Cordova vs ReactNative vs Xamarin vs DIY

标签: React Xamarin

你还在通过编写本地的iOS或Android代码来创建移动应用程序吗?对于那些平台特定的编程语言和工具、技能集问题、程序有非常具体的需要或者只是无知,或许这才是永远的爱。该是我们重新审视移动应用程序开发、选择合适的工具和框架集的时候了,以使我们的工作有更高的投资回报率(ROI)和更快的开发速度。

我和周围的很多开发者聊过,他们绝大多数都不喜欢用本地iOS(Swift、object C)、android(java)或者Windows(.Net)特定的编程语言和工具开发移动应用程序,因为他们的程序设计目标是多平台。原因在于本地开发需要每个平台对应自己特定的技能集,通常需要更多的开发努力。

几个月前,我们开始使用React Native,必须承认对于应用程序(app)开发来说,它的功能非常强大,使用起来也很愉快。自从Facebook用React native实现了对Android的支持起,我们基本上接受了它。尽管如此,React Native并非独一无二,还有另外一些很棒的选项,比如Xamarin。对于快速移动应用程序开发来说,Xamarin是一种功能最丰富、易于创建和使用的平台。

我在这里不会涉及所有可选的移动应用程序开发平台,只是分别用一个有代表性的平台来说明不同的开发方法,以便让你理解每一种开发方法的概念和方法论。

移动应用程序开发-平台的选择

说起移动应用程序开发,自然界中有许多平台和工具可以使用。在过去的几周里,我对各种开发方法进行了评估,并得出结论,有五种开发方法可供选择,列举如下:

  • 本地开发
  • Cordova :基于混合模式移动应用开发(Hybrid app)
  • 使用React Native进行移动应用程序开发
  • 使用Xamarin进行移动应用程序开发
  • DIY (Do It Yourself)工具:最后但并非最不重要

所有的工具都可以划归到上面提及的某一类中,它们提供了不同的功能集,需要特定的技能集来进行移动应用的设计与开发。在长期的移动应用开发策略中,首先选择正确的工具/平台是非常关键的因素。

选择哪个移动app开发更好?

选择不同的本地代码开发具有他们自己的优势和背后的道理。没有任何一个工具或者方法注定就是赢家,由于它依赖多种因素。在选择一个特定app开发平台或者方法最重要的因素依赖下面的关键点——

  • 个人或者开发团队的技能
  • app独一无二的需求
  • 目标用户划分
  • 个人或者组织的长期策略视野

如果你只是在一个特定平台开发apps,那使用native(swift,objective c 开发ios,java开发android)是最佳选择,因为你可以获得最佳性能、最新的APIs和工作特征。

你只是选择一个平台,那么,不必担心代码的复用。偏离本地主要理由是开发团队的技能,虽然不是唯一的一个。

让我们看看本地开发不同选择的替代方面。

基于Cordova的移动App开发

混合App开发使用纯的JavaScript、HTML和CSS ,它是最容易和最流行的开发方法之一,它在过去的网络开发中处于优势地位。有大量的框架采用了古老的纯的JavaScript和CSS,这些框架可以让你很轻松地进行移动App开发;其中的一些框架包括Ionic, kendo UI和 jQuery Mobile。

当然,你也可以选择不使用已有的框架,而是使用纯净的JavaScript、HTML和CSS,从头开始构建你的App部件。然而,使用像 Ionic、Kendo UI、 jQuery Mobile、 Onesen UI或者其它任何一种建立好的框架都会使你的开发时间以指数方式减少。

你或许想要查看这篇热门文章 – 最好的混合App开发框架

你的App如何访问硬件组成呢?

Cordova该要出场了。它提出了统一的JavaScript API,使用这些API可以访问像摄像头和加速度传感器一类的设备功能,在Android、iOS和Windows这样的几乎所有平台上都有加速度传感器。你一旦准备好App,就可以使用Cordova对其进行编译,Cordova会将App连同特定的JavaScript、CSS和HTML一起打包进平台特定的容器(webview)中。该容器在你的程序和硬件组成之间通过统一的JavaScript API架起了一座桥梁。

你得到的最大好处是只需要开发和维护单一的代码库,还有就是,如果你已经是一名Web开发者,就无需学习任何新技术了。

这里需要注意的关键点是程序运行在webview容器中,而并非运行在硬件本地,这使得它在性能上比原生Android程序或原生iOS程序要差。但是,所有的App并不都需要超级性能,对此你要首先做出权衡和决定。

Cordova遵循Apache许可证,自由且开源,它由Apache软件基金会发布。要阅读更多有关Cordova的内容可访问cordova.apache.org

使用React Native进行移动App开发

对于快速移动程序开发来说,React native相对较新,但却是一种优秀的开发方法,iOS和Android均可以使用。最近已增加了对于Android的支持。React native是来自于Facebook的开源框架,用来开发原生iOS和Android App。

尽管React Native是市场中的新成员,但是在全球范围内许多开发者和组织已经开始使用它进行跨平台移动App的开发,有一些App范例,比如SoundCloud Pulse、 Discovery VR、Facebook Ads Manager、 Bit Wallet、 Squad、Myntra和Running。

React native同样基于JavaScript,但只是纯的JavaScript、HTML5和CSS没有太多帮助,你需要投入一些时间学习更多的东西。你需要理解React,它是facebook为了开发网络程序于2013年发布的框架,React native是React本身的扩展,它们使用了同样的开发理念。

除了React框架,你还需要理解JSX,JSX是对ECMAScript进行了语法扩展的类XML。一旦你对JSX上了手,那么编写React native UI组件将变得绝对轻而易举。你也需要使用Xcode,它是用来构建Android版本App所需的iOS模拟器和命令行工具。另外,掌握程序架构框架的知识将有助于组织并加速App的开发,这些知识比如有Flux和与之相关联的类似Redux和Reflux这样的包。

React Native和Cordova或PhoneGap之间的比较

在Cordova中基于混合程序使用HTML和JavaScript组件开发的UI运行在Android和iOS平台的webview(内嵌在浏览器中)容器中。在React Native中同样如此,UI是用JavaScript react组件(JSX扩展)编写的,只不过每个React native UI组件和iOS以及Android的原生UI组件都是对应的。因此,用JavaScript编写的组件要转换成原生组件。这样我们就拥有了访问底层平台原生组件的JavaScript代码接口。

例如:创建标签-在react native中用TabBarIOS组件实现iOS中的UITabBar,用DrawerLayoutAndroid组件实现Android中的抽屉(Drawer)效果。

上面的例子也说明使用react native在Android和iOS之间的代码重用并非纯粹的100%,但是根据开发内容的不同,能够达到85%-100%这样的目标。

使用react native最大的好处在于仅仅使用JavaScript就可以同时开发iOS和Android版本的App。理念就是-“仅学习一次,就可以编写任何平台,并达到代码重用的最大化”。

更多有关React Native的知识可访问 react native

使用Xamarin进行移动app开发

Xamarin是用于构建原生移动App的另一平台,它是用于构建App的最有用的平台之一。使用Xamarin,可以通过C#语言编写原生Android、iOS、windows和Mac程序。

Xamarin的开发理念和React native基本一样,但是对技能集的要求有了彻底的改变,因为不再使用JavaScript,而是要使用C#和Microsoft工具。代码也不能100%重用,但80%的目标可以很容易达到。

使用Xamarin编写程序不是最容易的,但是可以让你编写出高性能的移动程序。用C#编写代码时,Xamarin编译器将代码编译成iOS、Android或者Windows Phone各自平台的原生包。对于iOS,会将代码直接编译成ARM汇编代码,所以它是纯碎的原生程序。

Xamarin.forms是由Xamarin团队最新推出的又一个功能,使用它开发原生的Android、iOS或Windows程序可以达到100%的代码重用。这些界面的UI控件在运行时会映射成原生控件,因此100%的代码重用是可能的。尽管它没有涵盖全部的原生控件,但已涵盖编写程序时要用到的90%的控件。

Xamarin是Microsoft收养的孩子。Microsoft最近收购了Xamarin,自此以后,Xamarin的用途会进一步增强,更多的功能会比以前增加的更快。

C#开发者可以自如地使用其中一个最高级的IDE-visual studio进行程序开发。Mac用户也可以使用具有相同丰富特性的Xamarin studio进行开发。

了解更多有关Xamarin的信息可以访问xamarin

使用DIY的APP构建工具进行移动APP开发

你自己的工具不够创新或者无法开发新的应用程序,但是,在短期内这些非常有利于从应用市场获取一个app。这些工具封装了大量模板,正在等待在苹果的应用市场或者谷歌play发布。

这些工具大多数是基于云app开发平台,保障你不用担忧要建立你的系统来构建移动应用。你自己的工具提供直观的app接口就可以用来构建app,不用任何先前的开发经验。

你可以选择一个app模板,以你的方式去自定义和配置他,全部使用你自己的设置。个人与小企业使用这些工具来创建移动apps, 并且更加快速推向市场。

UI/UX设计师总是依赖程序员来实现他们梦寐以求的app以推向市场,但是当今时代,UI/UX设计师可以使用拖拽和摆放工具来创建移动apps,向开发者边缘化。图像设计师可以采用app模板来创建和混合出漂亮好看的图形,使得app看起来独一无二(虽然不仅如此)。品牌与品牌之间的联系具体的图形和颜色,使得app独一无二。

在这个节奏快速的世界,当你的目标是创收,那它可能不总是专注于创新和创造,从零开始建立应用程序,使用现有的app模板来订制您的或者您的客户的需求,这样可能会更加高效。

你可能想查看这篇流行的文章-Top 10 DIY mobile app makers.

结论

应用开发和许多平台的选择都有很多的方法论,没有哪个是通用的。

很多开发者就是爱 Java ,并且持续用 Java 开发安卓应用,不认为有什么其他的选项,同样在 Objective-C 的开发者中也这样。不可否认,本地 app 开发带来的最佳性能和流畅体验。

随着像 Ionic,Onsen UI, Intel XDK 和 Sencha Touch 这些的混合框架的出现, Web 开发者变成移动开发者将不会有任何困难。所有这些框架的处理几乎都是与 JavaScript,CSS 和 HTML 相关,并且支持几乎所有的移动平台。

from:https://www.oschina.net/translate/mobile-app-development-cordova-vs-react-native-vs-xamarin

微服务化之缓存的设计

本文章为《互联网高并发微服务化架构实践》系列课程的第五篇

前四篇为:

微服务化的基石——持续集成

微服务的接入层设计与动静资源隔离

微服务化的数据库设计与读写分离

微服务化之无状态化与容器化

在高并发场景下,需要通过缓存来减少数据库的压力,使得大量的访问进来能够命中缓存,只有少量的需要到数据库层。由于缓存基于内存,可支持的并发量远远大于基于硬盘的数据库。所以对于高并发设计,缓存的设计时必不可少的一环。

一、为什么要使用缓存

为什么要使用缓存呢?源于人类的一个梦想,就是多快好省的建设社会主义。

多快好省?很多客户都这么要求,但是作为具体做技术的你,当然知道,好就不能快,多就没法省。

可是没办法,客户都这样要求:

这个能不能便宜一点,你咋这么贵呀,你看人家都很便宜的。(您好,这种打折的房间比较靠里,是不能面向大海的)

你们的性能怎么这么差啊,用你这个系统跑的这么慢,你看人家广告中说速度能达到多少多少。(您好,你如果买一个顶配的,我们也是有这种性能的)

你们服务不行啊,你就不能彬彬有礼,穿着整齐,送点水果瓜子啥的?(您好,我们兰州拉面馆没有这项服务,可以去对面的俏江南看一下)

这么贵的菜,一盘就这么一点点,都吃不饱,就不能上一大盘么。(您好,对面的兰州拉面10块钱一大碗)

怎么办呢?劳动人民还是很有智慧的,就是聚焦核心需求,让最最核心的部分享用好和快,而非核心的部门就多和省就可以了。

你可以大部分时间住在公司旁边的出租屋里面,但是出去度假的一个星期,选一个面朝大海,春暖花开的五星级酒店。

你可以大部分时间都挤地铁,挤公交,跋涉2个小时从北五环到南五环,但是有急事的时候,你可以打车,想旅游的时候,可以租车。

你可以大部分时间都吃普通的餐馆,而朋友来了,就去高级饭店里面搓一顿。

在计算机世界也是这样样子的,如图所示。

越是快的设备,存储量越小,越贵,而越是慢的设备,存储量越大,越便宜。

对于一家电商来讲,我们既希望存储越来越多的数据,因为数据将来就是资产,就是财富,只有有了数据,我们才知道用户需要什么,同时又希望当我想访问这些数据的时候,能够快速的得到,双十一拼的就是速度和用户体验,要让用户有流畅的感觉。

所以我们要讲大量的数据都保存下来,放在便宜的存储里面,同时将经常访问的,放在贵的,小的存储里面,当然贵的快的往往比较资源有限,因而不能长时间被某些数据长期霸占,所以要大家轮着用,所以叫缓存,也就是暂时存着。

二、都有哪些类型的缓存

当一个应用刚开始的时候,架构比较简单,往往就是一个Tomcat,后面跟着一个数据库。

简单的应用,并发量不大的时候,当然没有问题。

然而数据库相当于我们应用的中军大帐,是我们整个架构中最最关键的一部分,也是最不能挂,也最不能会被攻破的一部分,因而所有对数据库的访问都需要一道屏障来进行保护,常用的就是缓存。

我们以Tomcat为分界线,之外我们称为接入层,接入层当然应该有缓存,还有CDN,这个在这篇文章中有详细的描述,微服务的接入层设计与动静资源隔离

Tomcat之后,我们称为应用层,应用层也应该有缓存,这是我们这一节讨论的重点。

最简单的方式就是Tomcat里面有一层缓存,常称为本地缓存LocalCache。

这类的缓存常见的有Ehcache和Guava Cache,由于这类缓存在Tomcat本地,因而访问速度是非常快的。

但是本地缓存有个比较大的缺点,就是缓存是放在JVM里面的,会面临Full GC的问题,一旦出现了FullGC,就会对应用的性能和相应时间产生影响,当然也可以尝试jemalloc的分配方式。

还有一种方式,就是在Tomcat和Mysql中间加了一层Cache,我们常称为分布式缓存。

分布式缓存常见的有Memcached和Redis,两者各有优缺点。

Memcached适合做简单的key-value存储,内存使用率比较高,而且由于是多核处理,对于比较大的数据,性能较好。

但是缺点也比较明显,Memcached严格来讲没有集群机制,横向扩展完全靠客户端来实现。另外Memcached无法持久化,一旦挂了数据就都丢失了,如果想实现高可用,也是需要客户端进行双写才可以。

所以可以看出Memcached真的是设计出来,简简单单为了做一个缓存的。

Redis的数据结构就丰富的多了,单线程的处理所有的请求,对于比较大的数据,性能稍微差一点。

Redis提供持久化的功能,包括RDB的全量持久化,或者AOF的增量持久化,从而使得Redis挂了,数据是有机会恢复的。

Redis提供成熟的主备同步,故障切换的功能,从而保证了高可用性。

所以很多地方管Redis称为内存数据库,因为他的一些特性已经有了数据库的影子。

这也是很多人愿意用Redis的原因,集合了缓存和数据库的优势,但是往往会滥用这些优势,从而忽略了架构层面的设计,使得Redis集群有很大的风险。

很多情况下,会将Redis当做数据库使用,开启持久化和主备同步机制,以为就可以高枕无忧了。

然而Redis的持久化机制,全量持久化则往往需要额外较大的内存,而在高并发场景下,内存本来就很紧张,如果造成swap,就会影响性能。增量持久化也涉及到写磁盘和fsync,也是会拖慢处理的速度,在平时还好,如果高并发场景下,仍然会影响吞吐量。

所以在架构设计角度,缓存就是缓存,要意识到数据会随时丢失的,要意识到缓存的存着的目的是拦截到数据库的请求。如果为了保证缓存的数据不丢失,从而影响了缓存的吞吐量,甚至稳定性,让缓存响应不过来,甚至挂掉,所有的请求击穿到数据库,就是更加严重的事情了。

如果非常需要进行持久化,可以考虑使用levelDB此类的,对于随机写入性能较好的key-value持久化存储,这样只有部分的确需要持久化的数据,才进行持久化,而非无论什么数据,通通往Redis里面扔,同时统一开启了持久化。

三、基于缓存的架构设计要点

所以基于缓存的设计:

1、多层次

这样某一层的缓存挂了,还有另一层可以撑着,等待缓存的修复,例如分布式缓存因为某种原因挂了,因为持久化的原因,同步机制的原因,内存过大的原因等,修复需要一段时间,在这段时间内,至少本地缓存可以抗一阵,不至于一下子就击穿数据库。而且对于特别特别热的数据,热到导致集中式的缓存处理不过来,网卡也被打满的情况,由于本地缓存不需要远程调用,也是分布在应用层的,可以缓解这种问题。

2、分场景

到底要解决什么问题,可以选择不同的缓存。是要存储大的无格式的数据,还是要存储小的有格式的数据,还是要存储一定需要持久化的数据。具体的场景下一节详细谈。

3、要分片

使得每一个缓存实例都不大,但是实例数目比较多,这样一方面可以实现负载均衡,防止单个实例称为瓶颈或者热点,另一方面如果一个实例挂了,影响面会小很多,高可用性大大增强。分片的机制可以在客户端实现,可以使用中间件实现,也可以使用Redis的Cluster的方式,分片的算法往往都是哈希取模,或者一致性哈希。

四、缓存的使用场景

当你的应用扛不住,知道要使用缓存了,应该怎么做呢?

场景1:和数据库中的数据结构保持一致,原样缓存

这种场景是最常见的场景,也是很多架构使用缓存的适合,最先涉及到的场景。

基本就是数据库里面啥样,我缓存也啥样,数据库里面有商品信息,缓存里面也放商品信息,唯一不同的是,数据库里面是全量的商品信息,缓存里面是最热的商品信息。

每当应用要查询商品信息的时候,先查缓存,缓存没有就查数据库,查出来的结果放入缓存,从而下次就查到了。

这个是缓存最最经典的更新流程。这种方式简单,直观,很多缓存的库都默认支持这种方式。

场景2:列表排序分页场景的缓存

有时候我们需要获得一些列表数据,并对这些数据进行排序和分页。

例如我们想获取点赞最多的评论,或者最新的评论,然后列出来,一页一页的翻下去。

在这种情况下,缓存里面的数据结构和数据库里面完全不一样。

如果完全使用数据库进行实现,则按照某种条件将所有的行查询出来,然后按照某个字段进行排序,然后进行分页,一页一页的展示。

但是当数据量比较大的时候,这种方式往往成为瓶颈,首先涉及的数据库行数比较多,而且排序也是个很慢的活,尽管可能有索引,分页也是翻页到最后,越是慢。

在缓存里面,就没必要每行一个key了,而是可以使用Redis的列表方式进行存储,当然列表的长短是有限制的,肯定放不下数据库里面这么多,但是大家会发现其实对于所有的列表,用户往往没有耐心看个十页八页的,例如百度上搜个东西,也是有排序和分页的,但是你每次都往后翻了吗,每页就十条,就算是十页,或者一百页,也就一千条数据,如果保持ID的话,完全放的下。

如果已经排好序,放在Redis里面,那取出列表,翻页就非常快了。

可以后台有一个线程,异步的初始化和刷新缓存,在缓存里面保存一个时间戳,当有更新的时候,刷新时间戳,异步任务发现时间戳改变了,就刷新缓存。

场景3:计数缓存

计数对于数据库来讲,是一个非常繁重的工作,需要查询大量的行,最后得出计数的结论,当数据改变的时候,需要重新刷一遍,非常影响性能。

因此可以有一个计数服务,后端是一个缓存,将计数作为结果放在缓存里面,当数据有改变的时候,调用计数服务增加或者减少计数,而非通过异步数据库count来更新缓存。

计数服务可以使用Redis进行单个计数,或者hash表进行批量计数

场景4:重构维度缓存

有时候数据库里面保持的数据的维度是为了写入方便,而非为了查询方便的,然而同时查询过程,也需要处理高并发,因而需要为了查询方便,将数据重新以另一个维度存储一遍,或者说将多给数据库的内容聚合一下,再存储一遍,从而不用每次查询的时候都重新聚合,如果还是放在数据库,比较难维护,放在缓存就好一些。

例如一个商品的所有的帖子和帖子的用户,以及一个用户发表过的所有的帖子就是属于两个维度。

这需要写入一个维度的时候,同时异步通知,更新缓存中的另一个维度。

在这种场景下,数据量相对比较大,因而单纯用内存缓存memcached或者redis难以支撑,往往会选择使用levelDB进行存储,如果levelDB的性能跟不上,可以考虑在levelDB之前,再来一层memcached。

场景5:较大的详情内容数据缓存

对于评论的详情,或者帖子的详细内容,属于非结构化的,而且内容比较大,因而使用memcached比较好。

五、缓存三大矛盾问题

1、缓存实时性和一致性问题:当有了写入后咋办?

虽然使用了缓存,大家心里都有一个预期,就是实时性和一致性得不到完全的保证,毕竟数据保存了多份,数据库一份,缓存中一份,当数据库中因写入而产生了新的数据,往往缓存是不会和数据库操作放在一个事务里面的,如何将新的数据更新到缓存里面,什么时候更新到缓存里面,不同的策略不一样。

从用户体验角度,当然是越实时越好,用户体验越流畅,完全从这个角度出发,就应该有了写入,马上废弃缓存,触发一次数据库的读取,从而更新缓存。但是这和第三个问题,高并发就矛盾了,如果所有的都实时从数据库里面读取,高并发场景下,数据库往往受不了。

2、缓存的穿透问题:当没有读到咋办?

为什么会出现缓存读取不到的情况呢?

第一:可能读取的是冷数据,原来从来没有访问过,所以需要到数据库里面查询一下,然后放入缓存,再返回给客户。

第二:可能数据因为有了写入,被实时的从缓存中删除了,就如第一个问题中描述的那样,为了保证实时性,当数据库中的数据更新了之后,马上删除缓存中的数据,导致这个时候的读取读不到,需要到数据库里面查询后,放入缓存,再返回给客户。

第三:可能是缓存实效了,每个缓存数据都会有实效时间,过了一段时间没有被访问,就会失效,这个时候数据就访问不到了,需要访问数据库后,再放入缓存。

第四:数据被换出,由于缓存内存是有限的,当使用快满了的时候,就会使用类似LRU策略,将不经常使用的数据换出,所以也要访问数据库。

第五:后端确实也没有,应用访问缓存没有,于是查询数据库,结果数据库里面也没有,只好返回客户为空,但是尴尬的是,每次出现这种情况的时候,都会面临着一次数据库的访问,纯属浪费资源,常用的方法是,讲这个key对应的结果为空的事实也进行缓存,这样缓存可以命中,但是命中后告诉客户端没有,减少了数据库的压力。

无论哪种原因导致的读取缓存读不到的情况,该怎么办?是个策略问题。

一种是同步访问数据库后,放入缓存,再返回给客户,这样实时性最好,但是给数据库的压力也最大。

另一种方式就是异步的访问数据库,暂且返回客户一个fallback值,然后同时触发一个异步更新,这样下次就有了,这样数据库压力小很多,但是用户就访问不到实时的数据了。

3、缓存对数据库高并发访问:都来访问数据库咋办?

我们本来使用缓存,是来拦截直接访问数据库请求的,从而保证数据库大本营永远处于健康的状态。但是如果一遇到不命中,就访问数据库的话,平时没有什么问题,但是大促情况下,数据库是受不了的。

一种情况是多个客户端,并发状态下,都不命中了,于是并发的都来访问数据库,其实只需要访问一次就好,这种情况可以通过加锁,只有一个到后端来实现。

另外就是即便采取了上述的策略,依然并发量非常大,后端的数据库依然受不了,则需要通过降低实时性,将缓存拦在数据库前面,暂且撑住,来解决。

六、解决缓存三大矛盾的刷新策略

1、实时策略

所谓的实时策略,是平时缓存使用的最常用的策略,也是保持实时性最好的策略。

读取的过程,应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。如果命中,应用程序从cache中取数据,取到后返回。

写入的过程,把数据存到数据库中,成功后,再让缓存失效,失效后下次读取的时候,会被写入缓存。那为什么不直接写缓存呢?因为如果两个线程同时更新数据库,一个将数据库改为10,一个将数据库改为20,数据库有自己的事务机制,可以保证如果20是后提交的,数据库里面改为20,但是回过头来写入缓存的时候就没有事务了,如果改为20的线程先更新缓存,改为10的线程后更新缓存,于是就会长时间出现缓存中是10,但是数据库中是20的现象。

这种方式实时性好,用户体验好,是默认应该使用的策略。

2、异步策略

所谓异步策略,就是当读取的时候读不到的时候,不直接访问数据库,而是返回一个fallback数据,然后往消息队列里面放入一个数据加载的事件,在背后有一个任务,收到事件后,会异步的读取数据库,由于有队列的作用,可以实现消峰,缓冲对数据库的访问,甚至可以将多个队列中的任务合并请求,合并更新缓存,提高了效率。

当更新的时候,异步策略总是先更新数据库和缓存中的一个,然后异步的更新另一个。

一是先更新数据库,然后异步更新缓存。当数据库更新后,同样生成一个异步消息,放入消息队列中,等待背后的任务通过消息进行缓存更新,同样可以实现消峰和任务合并。缺点就是实时性比较差,估计要过一段时间才能看到更新,好处是数据持久性可以得到保证。

一是先更新缓存,然后异步更新数据库。这种方式读取和写入都用缓存,将缓存完全挡在了数据库的前面,把缓存当成了数据库在用。所以一般会使用有持久化机制和主备的redis,但是仍然不能保证缓存不丢数据,所以这种情况适用于并发量大,但是数据没有那么关键的情况,好处是实时性好。

在实时策略扛不住大促的时候,可以根据场景,切换到上面的两种模式的一个,算是降级策略。

3、定时策略

如果并发量实在太大,数据量也大的情况,异步都难以满足,可以降级为定时刷新的策略,这种情况下,应用只访问缓存,不访问数据库,更新频率也不高,而且用户要求也不高,例如详情,评论等。

这种情况下,由于数据量比较大,建议将一整块数据拆分成几部分进行缓存,而且区分更新频繁的和不频繁的,这样不用每次更新的时候,所有的都更新,只更新一部分。并且缓存的时候,可以进行数据的预整合,因为实时性不高,读取预整合的数据更快。

有关缓存就说到这里,下一节讲分布式事务。

from:https://mp.weixin.qq.com/s/-9wHpKGf7aJSbtShpCcoVg

每个程序员都应该了解的内存知识-0

这个系列文章源于 What Every Programmer Should Know About Memory ,粗读下来觉得很不错,最好能留存下来。同时发现这个系列的文章已经有一部分被人翻译了。故在此转发留存一份,毕竟存在自己收留的才是最可靠的,经常发现很多不错的文章链接失效的情况。

本文转载自,翻译自。本人进行了轻微的修改,感觉更符合原义。

1 简介

早期计算机比现在更为简单。系统的各种组件例如CPU,内存,大容量存储器和网口,由于被共同开发因而有非常均衡的表现。例如,内存和网口并不比CPU在提供数据的时候更(特别的)快。

曾今计算机稳定的基本结构悄然改变,硬件开发人员开始致力于优化单个子系统。于是电脑一些组件的性能大大的落后因而成为了瓶颈。由于开销的原因,大容量存储器和内存子系统相对于其他组件来说改善得更为缓慢。

大容量存储的性能问题往往靠软件来改善: 操作系统将常用(且最有可能被用)的数据放在主存中,因为后者的速度要快上几个数量级。或者将缓存加入存储设备中,这样就可以在不修改操作系统的前提下提升性能。(然而,为了在使用缓存时保证数据的完整性,仍然要作出一些修改)。这些内容不在本文的谈论范围之内,就不作赘述了。

而解决内存的瓶颈更为困难,它与大容量存储不同,几乎每种方案都需要对硬件作出修改。目前,这些变更主要有以下这些方式:

  • RAM的硬件设计(速度与并发度)
  • 内存控制器的设计
  • CPU缓存
  • 设备的直接内存访问(DMA)

本文主要关心的是CPU缓存和内存控制器的设计。在讨论这些主题的过程中,我们还会研究DMA。不过,我们首先会从当今商用硬件的设计谈起。这有助于我们理解目前在使用内存子系统时可能遇到的问题和限制。我们还会详细介绍RAM的分类,说明为什么会存在这么多不同类型的内存。

本文不会包括所有内容,也不会包括最终性质的内容。我们的讨论范围仅止于商用硬件,而且只限于其中的一小部分。另外,本文中的许多论题,我们只会点到为止,以达到本文目标为标准。对于这些论题,大家可以阅读其它文档,获得更详细的说明。

当本文提到操作系统特定的细节和解决方案时,针对的都是Linux。无论何时都不会包含别的操作系统的任何信息,作者无意讨论其他操作系统的情况。如果读者认为他/她不得不使用别的操作系统,那么必须去要求供应商提供其操作系统类似于本文的文档。

在开始之前最后的一点说明,本文包含大量出现的术语“通常”和别的类似的限定词。这里讨论的技术在现实中存在于很多不同的实现,所以本文只阐述使用得最广泛最主流的版本。在阐述中很少有地方能用到绝对的限定词。

1.1 文档结构

这个文档主要视为软件开发者而写的。本文不会涉及太多硬件细节,所以喜欢硬件的读者也许不会觉得有用。但是在我们讨论一些有用的细节之前,我们先要描述足够多的背景。

在这个基础上,本文的第二部分将描述RAM(随机寄存器)。懂得这个部分的内容很好,但是此部分的内容并不是懂得其后内容必须部分。我们会在之后引用不少之前的部分,所以心急的读者可以跳过任何章节来读他们认为有用的部分。

第三部分会谈到不少关于CPU缓存行为模式的内容。我们会列出一些图标,这样你们不至于觉得太枯燥。第三部分对于理解整个文章非常重要。第四部分将简短的描述虚拟内存是怎么被实现的。这也是你们需要理解全文其他部分的背景知识之一。

第五部分会提到许多关于Non Uniform Memory Access (NUMA)系统。

第六部分是本文的中心部分。在这个部分里面,我们将回顾其他许多部分中的信息,并且我们将给阅读本文的程序员许多在各种情况下的编程建议。如果你真的很心急,那么你可以直接阅读第六部分,并且我们建议你在必要的时候回到之前的章节回顾一下必要的背景知识。

本文的第七部分将介绍一些能够帮助程序员更好的完成任务的工具。即便在彻底理解了某一项技术的情况下,距离彻底理解在非测试环境下的程序还是很遥远的。我们需要借助一些工具。

第八部分,我们将展望一些在未来我们可能认为好用的科技。

1.2 反馈问题

作者会不定期更新本文档。这些更新既包括伴随技术进步而来的更新也包含更改错误。非常欢迎有志于反馈问题的读者发送电子邮件。

1.3 致谢

我首先需要感谢Johnray Fuller尤其是Jonathan Corbet,感谢他们将作者的英语转化成为更为规范的形式。Markus Armbruster提供大量本文中对于问题和缩写有价值的建议。

1.4 关于本文

本文题目对David Goldberg的经典文献《What Every Computer Scientist Should Know About Floating-Point Arithmetic》[goldberg]表示致敬。Goldberg的论文虽然不普及,但是对于任何有志于严格编程的人都会是一个先决条件

2 商用硬件现状

鉴于目前专业硬件正在逐渐淡出,理解商用硬件的现状变得十分重要。现如今,人们更多的采用水平扩展,也就是说,用大量小型、互联的商用计算机代替巨大、超快(但超贵)的系统。原因在于,快速而廉价的网络硬件已经崛起。那些大型的专用系统仍然有一席之地,但已被商用硬件后来居上。2007年,Red Hat认为,未来构成数据中心的“积木”将会是拥有最多4个插槽的计算机,每个插槽插入一个四核CPU,这些CPU都是超线程的。(超线程使单个处理器核心能同时处理两个以上的任务,只需加入一点点额外硬件)。也就是说,这些数据中心中的标准系统拥有最多64个虚拟处理器(至今来看2018那年,96核/128核的服务已经是很常见的服务器配置了)。当然可以支持更大的系统,但人们认为4插槽、4核CPU是最佳配置,绝大多数的优化都针对这样的配置。

在不同商用计算机之间,也存在着巨大的差异。不过,我们关注在主要的差异上,可以涵盖到超过90%以上的硬件。需要注意的是,这些技术上的细节往往日新月异,变化极快,因此大家在阅读的时候也需要注意本文的写作时间。

这么多年来,个人计算机和小型服务器被标准化到了一个芯片组上,它由两部分组成: 北桥和南桥,见图2.1。

图2.1 北桥和南桥组成的结构

CPU通过一条通用总线(前端总线,FSB)连接到北桥。北桥主要包括内存控制器和其它一些组件,内存控制器决定了RAM芯片的类型。不同的类型,包括DRAM、Rambus和SDRAM等等,要求不同的内存控制器。

为了连通其它系统设备,北桥需要与南桥通信。南桥又叫I/O桥,通过多条不同总线与设备们通信。目前,比较重要的总线有PCI、PCI Express、SATA和USB总线,除此以外,南桥还支持PATA、IEEE 1394、串行口和并行口等。比较老的系统上有连接北桥的AGP槽。那是由于南北桥间缺乏高速连接而采取的措施。现在的PCI-E都是直接连到南桥的。

这种结构有一些需要注意的地方:

  • 从某个CPU到另一个CPU的数据需要走它与北桥通信的同一条总线。
  • 与RAM的通信需要经过北桥
  • RAM只有一个端口。(本文不会介绍多端口RAM,因为商用硬件不采用这种内存,至少程序员无法访问到。这种内存一般在路由器等专用硬件中采用。)
  • CPU与南桥设备间的通信需要经过北桥

在上面这种设计中,瓶颈马上出现了。第一个瓶颈与设备对RAM的访问有关。早期,所有设备之间的通信都需要经过CPU,结果严重影响了整个系统的性能。为了解决这个问题,有些设备加入了直接内存访问(DMA)的能力。DMA允许设备在北桥的帮助下,无需CPU的干涉,直接读写RAM。到了今天,所有高性能的设备都可以使用DMA。虽然DMA大大降低了CPU的负担,却占用了北桥的带宽,与CPU形成了争用。

第二个瓶颈来自北桥与RAM间的总线。总线的具体情况与内存的类型有关。在早期的系统上,只有一条总线,因此不能实现并行访问。近期的RAM需要两条独立总线(或者说通道,DDR2就是这么叫的,见图2.8),可以实现带宽加倍。北桥将内存访问交错地分配到两个通道上。更新的内存技术(如FB-DRAM)甚至加入了更多的通道。

由于带宽有限,我们需要以一种使延迟最小化的方式来对内存访问进行调度。我们将会看到,处理器的速度比内存要快得多,需要等待内存。如果有多个超线程核心或CPU同时访问内存,等待时间则会更长。对于DMA也是同样。

除了并发以外,访问模式也会极大地影响内存子系统、特别是多通道内存子系统的性能。关于访问模式,可参见2.2节。

在一些比较昂贵的系统上,北桥自己不含内存控制器,而是连接到外部的多个内存控制器上(在下例中,共有4个)。

图2.2 拥有外部控制器的北桥

这种架构的好处在于,多条内存总线的存在,使得总带宽也随之增加了。而且也可以支持更多的内存。通过同时访问不同内存区,还可以降低延时。对于像图2.2中这种多处理器直连北桥的设计来说,尤其有效。而这种架构的局限在于北桥的内部带宽,非常巨大(来自Intel)。(出于完整性的考虑,还需要补充一下,这样的内存控制器布局还可以用于其它用途,比如说「内存RAID」,它可以与热插拔技术一起使用。)

使用外部内存控制器并不是唯一的办法,另一个最近比较流行的方法是将控制器集成到CPU内部,将内存直连到每个CPU。这种架构的走红归功于基于AMD Opteron处理器的SMP系统。图2.3展示了这种架构。Intel则会从Nehalem处理器开始支持通用系统接口(CSI),基本上也是类似的思路——集成内存控制器,为每个处理器提供本地内存。

图2.3 集成的内存控制器

通过采用这样的架构,系统里有几个处理器,就可以有几个内存库(memory bank)。比如,在4 CPU的计算机上,不需要一个拥有巨大带宽的复杂北桥,就可以实现4倍的内存带宽。另外,将内存控制器集成到CPU内部还有其它一些优点,这里就不赘述了。

同样也有缺点。首先,系统仍然要让所有内存能被所有处理器所访问,导致内存不再是统一的资源(NUMA即得名于此)。处理器能以正常的速度访问本地内存(连接到该处理器的内存)。但它访问其它处理器的内存时,却需要使用处理器之间的互联通道。比如说,CPU 1 如果要访问CPU 2 的内存,则需要使用它们之间的互联通道。如果它需要访问CPU 4 的内存,那么需要跨越两条互联通道。

使用互联通道是有代价的。在讨论访问远端内存的代价时,我们用「NUMA因子」这个词。在图2.3中,每个CPU有两个层级: 相邻的CPU,以及两个互联通道外的CPU。在更加复杂的系统中,层级也更多。甚至有些机器有不止一种连接,比如说IBM的x445和SGI的Altix系列。CPU被归入节点,节点内的内存访问时间是一致的,或者只有很小的NUMA因子。而在节点之间的连接代价很大,而且有巨大的NUMA因子。

目前,已经有商用的NUMA计算机,而且它们在未来应该会扮演更加重要的角色。人们预计,从2008年底开始,每台SMP机器都会使用NUMA。每个在NUMA上运行的程序都应该认识到NUMA的代价。在第5节中,我们将讨论更多的架构,以及Linux内核为这些程序提供的一些技术。

除了本节中所介绍的技术之外,还有其它一些影响RAM性能的因素。它们无法被软件所左右,所以没有放在这里。如果大家有兴趣,可以在第2.1节中看一下。介绍这些技术,仅仅是因为它们能让我们绘制的RAM技术全图更为完整,或者是可能在大家购买计算机时能够提供一些帮助。

以下的两节主要介绍一些入门级的硬件知识,同时讨论内存控制器与DRAM芯片间的访问协议。这些知识解释了内存访问的原理,程序员可能会得到一些启发。不过,这部分并不是必读的,心急的读者可以直接跳到第2.2.5节。

2.1 RAM类型

这些年来,出现了许多不同类型的RAM,各有差异,有些甚至有非常巨大的不同。那些很古老的类型已经乏人问津,我们就不仔细研究了。我们主要专注于几类现代RAM,剖开它们的表面,研究一下内核和应用开发人员们可以看到的一些细节。

第一个有趣的细节是,为什么在同一台机器中有不同的RAM?或者说得更详细一点,为什么既有静态RAM(SRAM {SRAM还可以表示「同步内存」。}),又有动态RAM(DRAM)。功能相同,前者更快。那么,为什么不全部使用SRAM?答案是,代价。无论在生产还是在使用上,SRAM都比DRAM要贵得多。生产和使用,这两个代价因子都很重要,后者则是越来越重要。为了理解这一点,我们分别看一下SRAM和DRAM一个位的存储的实现过程。

在本节的余下部分,我们将讨论RAM实现的底层细节。我们将尽量控制细节的层面,比如,在「逻辑的层面」讨论信号,而不是硬件设计师那种层面,因为那毫无必要。

2.1.1 静态RAM

图2.4 6-T静态RAM

图2.4展示了6晶体管SRAM的一个单元。核心是4个晶体管M 1 – M 4 ,它们组成两个交叉耦合的反相器。它们有两个稳定的状态,分别代表0和1。只要保持V dd 有电,状态就是稳定的。

当访问单元的状态时,需要拉升WL的电平。使得

和 上可以读取状态。如果需要覆盖单元状态,先将 和 设置为期望的值,然后升起WL电平。由于外部的驱动强于内部的4个晶体管(M 1 – M 4

),所以旧状态会被覆盖。

更多详情,可以参考[sramwiki]。为了下文的讨论,需要注意以下问题:

  • 一个单元需要6个晶体管。也有采用4个晶体管的SRAM,但有缺陷。
  • 维持状态需要恒定的电源。
  • 升起WL后立即可以读取状态。信号与其它晶体管控制的信号一样,是直角的(快速在两个状态间变化)。
  • 状态稳定,不需要刷新循环。

SRAM也有其它形式,不那么费电,但比较慢。由于我们需要的是快速RAM,因此不在关注范围内。这些较慢的SRAM的主要优点在于接口简单,比动态RAM更容易使用。

2.1.2 动态RAM

动态RAM比静态RAM要简单得多。图2.5展示了一种普通DRAM的结构。它只含有一个晶体管和一个电容器。显然,这种复杂性上的巨大差异意味着功能上的迥异。

图2.5 1-T动态RAM

动态RAM的状态是保持在电容器C中。晶体管M用来控制访问。如果要读取状态,拉升访问线AL,这时,可能会有电流流到数据线DL上,也可能没有,取决于电容器是否有电。如果要写入状态,先设置DL,然后升起AL一段时间,直到电容器充电或放电完毕。

动态RAM的设计有几个复杂的地方。由于读取状态时需要对电容器放电,所以这一过程不能无限重复,不得不在某个点上对它重新充电。

更糟糕的是,为了容纳大量单元(现在一般在单个芯片上容纳10的9次方以上的RAM单元),电容器的容量必须很小(0.000000000000001法拉以下)。这样,完整充电后大约持有几万个电子。即使电容器的电阻很大(若干兆欧姆),仍然只需很短的时间就会耗光电荷,称为「泄漏」。

这种泄露就是现在的大部分DRAM芯片每隔64ms就必须进行一次刷新的原因。在刷新期间,对于该芯片的访问是不可能的,这甚至会造成半数任务的延宕。(相关内容请察看【highperfdram】一章)

这个问题的另一个后果就是无法直接读取芯片单元中的信息,而必须通过信号放大器将0和1两种信号间的电势差增大。

最后一个问题在于电容器的冲放电是需要时间的,这就导致了信号放大器读取的信号并不是典型的矩形信号。所以当放大器输出信号的时候就需要一个小小的延宕,相关公式如下

这就意味着需要一些时间(时间长短取决于电容C和电阻R)来对电容进行冲放电。另一个负面作用是,信号放大器的输出电流不能立即就作为信号载体使用。图2.6显示了冲放电的曲线,x轴表示的是单位时间下的R*C。

与静态RAM可以即刻读取数据不同的是,当要读取动态RAM的时候,必须花一点时间来等待电容的冲放电完全。这一点点的时间最终限制了DRAM的速度。

当然了,这种读取方式也是有好处的。最大的好处在于缩小了规模。一个动态RAM的尺寸是小于静态RAM的。这种规模的减小不单单建立在动态RAM的简单结构之上,也是由于减少了静态RAM的各个单元独立的供电部分。以上也同时导致了动态RAM模具的简单化。

综上所述,由于不可思议的成本差异,除了一些特殊的硬件(包括路由器什么的)之外,我们的硬件大多是使用DRAM的。这一点深深的影响了咱们这些程序员,后文将会对此进行讨论。在此之前,我们还是先了解下DRAM的更多细节。

2.1.3 DRAM 访问

一个程序选择了一个内存位置使用到了一个虚拟地址。处理器转换这个到物理地址最后将内存控制选择RAM芯片匹配了那个地址。在RAM芯片去选择单个内存单元,部分的物理地址以许多地址行的形式被传递。

它单独地去处理来自于内存控制器的内存位置将完全不切实际:4G的RAM将需要 $ 2^32 $ 地址行。地址传递DRAM芯片的这种方式首先必须被路由器解析。一个路由器的N多地址行将有$ 2^N $输出行。这些输出行能被使用到选择内存单元。使用这个直接方法对于小容量芯片不再是个大问题。

from:https://www.tuicool.com/articles/IfueY3A

HTTP协议

我们知道目前很多应用系统中的内容传输协议采用的HTTP协议,因此不管你是前端人员、后端人员、运维人员,甚至是管理人员,都需要掌握HTTP知识!!

HTTP发展历史

HTTP/0.9  

该版本只有一个命令GET;没有HEADER等描述数据的信息; 服务器发送完毕,就关闭TCP连接。

HTTP/1.0  

该版本增加了很多命令;增加status code 和header;多字符集支持、多部分发送、权限、缓存等。

HTTP/1.1   

该版本增加了持久连接Pipeline,增加host和其他一些命令。持久连接会在HTTP特性中介绍;如果没有pipeline,那么Web服务器就需要串行处理请求,而有了pipeline,Web服务器就并行处理请求;而增加host实现了一台物理设备可以运行多个web服务。

HTTP/2.0   

所有数据以二进制传输,之前版本使用字符串进行传输;同一个连接 里面发送多个请求不再需要按照顺序来;头信息压缩以及推送等提高效率的功能。

HTTP三次握手

为什么要三次握手?因为网络是有可能延迟的,当客户端没有收到服务端的确认包,如果没有第三次握手,那么服务端不知道上次传输是不是被客户端正常接收了,如果没有接收,服务端的这个端口也是打开的,这就比较浪费资源。

HTTP报文

HTTP报文分为请求报文响应报文,请求报文和响应报文分为起始行、首部(header)和主体(body),请求报文的首部包括三部分,分别是HTTP方法、资源目录和协议,而响应报文的首部包括协议版本、状态码和状态吗对应的意思,比如200状态的意思是ok。需要注意的是:HTTP header和HTTP body之间以一行分隔。

HTTP方法  

HTTP方法定义对资源的操作,常用的有GET、POST等,这就就不详细展开了。

HTTP Code  

HTTP Code用于定义服务器对请求的处理结果,各个区间的code有不用的语义。1xx  表示信息响应类,表示接收到请求并且继续处理;2xx 表示成功;3xx 表示重定向;4xx 表示客户端出错;5xx 表示服务器出错。

HTTP特性

跨域请求   

同源策略,也就是说当两个请求的URL的协议、host和端口都相同的情况下,我们才认为这两个请求是同域的即同源,而只要协议、host和端口只要有一项是不同的,我们就认为是不同源的,即跨域,例如:

http://www.mukedada.com:80

http://www.mukedada.com:8080

上述两个请求就是跨域请求。需要注意的是跨域请求不是说浏览器限制了发起跨站请求,浏览器只是将返回结果拦截下来,最好的例子就是CSRF跨站脚本攻击。如果我们想让浏览器放行返回结果,则通过以下方法:

  1. 服务端设置Access-Control-Allow-Origin参数为允许,例如’Access-Control-Allow-Origin’ : ‘*’
  2. <link>、<img>和<script>三标签中的请求是允许跨域的,这也是JSONP的跨域做法。
Cache  Control 

对于静态资源,比如说image、js等,它们是不会经常方式变更的,而且它们的容量比较大,如果我们每次访问都要从服务器从获取相应数据,那么性能就会变得比较差,因此HTTP协议定义一些和缓存相关的参数。

可缓存性,表示在哪些地方可以缓存,比如说客户端浏览器、代理服务器等,它有三个常用的参数:public、private、no-cache。public 表明响应可以被任何对象缓存,包括发送请求的客户端浏览器、代理服务器等等;private 表示响应只能被单个用户缓存,不能作为共享缓存,即代理服务器不能缓存它;no-cache表明强制所有缓存了该响应的缓存用户,在使用已存储的缓存之前,发送带验证器的请求到源始服务器。

到期,max-age=<seconds>,设置缓存存储的最大周期,超过这个时间缓存就被认为过期。s-maxage=<seconds> 它的作用域仅在共享缓存(比如各个代理)。max-stale=<seconds> 表明客户端愿意接收一个已过期的资源。

验证,must-revalidate,缓存必须在使用之前验证旧资源的状态,并且不可使用过期资源。proxy-revalidate,与must-revalidate作用相同,但它仅适用于共享缓存(例如代理),并被私有缓存忽略。

其他。no-store,客户端和代理服务器不存储任何缓存,而是直接从服务器获取内容。no-transform:不得对资源进行转换或转变。Content-Encoding, Content-Range, Content-Type等HTTP头不能由代理修改。例如,非透明代理可以对图像格式进行转换,以便节省缓存空间或者减少缓慢链路上的流量。 no-transform指令不允许这样做。

Cookie   

服务端通过Set-Cookie将相关数据保存到浏览器中,而这些相关数据就是Cookie,那么,下次在同域的请求中就会带上这些Cookie,Cookie是键值对,可以设置多个。Cookie中通过max-age和expires设置过期时间,Secure值在https的时候发送,HttpOnly无法通过document.cookie访问。具体可以参考Session 和 Cookie

资源验证  

在Cache Control中我们介绍当设置no-cache参数时,表明每次请求都要到服务器验证,验证结果表明可以读取本地缓存才可以从本地读取缓存。只有到数据发生修改时,我们才需要从服务端读取最新数据,否则从本地读取缓存。此时,判断数据是否发生修改就变得尤为重要,通常我们采用Last-Modfied和Etag两个验证头来验证数据是否发生修改。其中Last-Modifed 通常配合If-Modified-Since或者If-UnModified-Since使用,而Etag 通常配合If-Match或者If-Non-Match使用。为了帮助大家理解,我举一个栗子。假设我们访问mudedada.com返回头信息包含:

Last-Modified:888

Etag:123

下一次访问mukedada.com的请求头中就会包含:

If-Modified-Since:888

If-Non-Match:123

服务器会比较请求头中的Last-Modified、Etag 和服务器中的对应值是否相同,如果不相同则重新获取,否则从本地缓存中获取。

长连接   

我们知道一个HTTP需要创建一个TCP连接,完成之后就关闭TCP连接,这个成本比较高(因为创建一个TCP连接需要通过三次握手),所以在HTTP/1.1开始支持长连接,请求头标识是Connection:keep-alive。如下图所示,同一个Connection ID表示同一个连接。需要注意的是同一个连接只能是同域请求。

数据协商   

数据协商指的是客户端向服务端发送请求时,客户端会声明它希望服务端返回个格式是什么?服务端根据客户端的声明来判断返回什么要的数据。其中客户端通过Accept、Accept-Encoding等参数进行设置,而服务端通过Content-Type等参数进行设置。

客户端相关参数

  1. Accept指定返回数据类型;
  2. Accept-Encoding指定服务端的数据压缩方式,目前服务端的压缩算法有gzip, deflate, br等;
  3. Accept-Language指定返回数据的语言,例如 Accept-Language:  zh-CN,zh;q=0.9,en;q=0.8,其中q表示的是权重,也就是说浏览器更希望服务器返回的是中文;
  4. User-Agent表示浏览器的相关信息,它能区分是移动端浏览器还是PC端浏览器,从而返回特定的页面。

服务端相关参数:

  1. Content-Type指的是服务端返回的数据类型;
  2. Content-Encoding对应客户端的Accept-Encoding,指的是数据压缩方式;
  3. Content-Language服务端语言。

from:https://mp.weixin.qq.com/s/vRQ2zuKxyLaBxcm9lolL7w

分布式锁

转载请务必注明原创地址为:http://www.54tianzhisheng.cn/2018/04/24/Distributed_lock/

什么是锁?

  • 在单进程的系统中,当存在多个线程可以同时改变某个变量(可变共享变量)时,就需要对变量或代码块做同步,使其在修改这种变量时能够线性执行消除并发修改变量。
  • 而同步的本质是通过锁来实现的。为了实现多个线程在一个时刻同一个代码块只能有一个线程可执行,那么需要在某个地方做个标记,这个标记必须每个线程都能看到,当标记不存在时可以设置该标记,其余后续线程发现已经有标记了则等待拥有标记的线程结束同步代码块取消标记后再去尝试设置标记。这个标记可以理解为锁。
  • 不同地方实现锁的方式也不一样,只要能满足所有线程都能看得到标记即可。如 Java 中 synchronize 是在对象头设置标记,Lock 接口的实现类基本上都只是某一个 volitile 修饰的 int 型变量其保证每个线程都能拥有对该 int 的可见性和原子修改,linux 内核中也是利用互斥量或信号量等内存数据做标记。
  • 除了利用内存数据做锁其实任何互斥的都能做锁(只考虑互斥情况),如流水表中流水号与时间结合做幂等校验可以看作是一个不会释放的锁,或者使用某个文件是否存在作为锁等。只需要满足在对标记进行修改能保证原子性和内存可见性即可。

什么是分布式?

分布式的 CAP 理论告诉我们:

任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。

目前很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。基于 CAP理论,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证最终一致性。

分布式场景

此处主要指集群模式下,多个相同服务同时开启.

在许多的场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务分布式锁等。很多时候我们需要保证一个方法在同一时间内只能被同一个线程执行。在单机环境中,通过 Java 提供的并发 API 我们可以解决,但是在分布式环境下,就没有那么简单啦。

  • 分布式与单机情况下最大的不同在于其不是多线程而是多进程
  • 多线程由于可以共享堆内存,因此可以简单的采取内存作为标记存储位置。而进程之间甚至可能都不在同一台物理机上,因此需要将标记存储在一个所有进程都能看到的地方。

什么是分布式锁?

  • 当在分布式模型下,数据只有一份(或有限制),此时需要利用锁的技术控制某一时刻修改数据的进程数。
  • 与单机模式下的锁不仅需要保证进程可见,还需要考虑进程与锁之间的网络问题。(我觉得分布式情况下之所以问题变得复杂,主要就是需要考虑到网络的延时和不可靠。。。一个大坑)
  • 分布式锁还是可以将标记存在内存,只是该内存不是某个进程分配的内存而是公共内存如 Redis、Memcache。至于利用数据库、文件等做锁与单机的实现是一样的,只要保证标记能互斥就行。

我们需要怎样的分布式锁?

  • 可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。
  • 这把锁要是一把可重入锁(避免死锁)
  • 这把锁最好是一把阻塞锁(根据业务需求考虑要不要这条)
  • 这把锁最好是一把公平锁(根据业务需求考虑要不要这条)
  • 有高可用的获取锁和释放锁功能
  • 获取锁和释放锁的性能要好

基于数据库做分布式锁

基于乐观锁

基于表主键唯一做分布式锁

利用主键唯一的特性,如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,当方法执行完毕之后,想要释放锁的话,删除这条数据库记录即可。

上面这种简单的实现有以下几个问题:

  • 这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
  • 这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
  • 这把锁只能是非阻塞的,因为数据的 insert 操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
  • 这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。
  • 这把锁是非公平锁,所有等待锁的线程凭运气去争夺锁。
  • 在 MySQL 数据库中采用主键冲突防重,在大并发情况下有可能会造成锁表现象。

当然,我们也可以有其他方式解决上面的问题。

  • 数据库是单点?搞两个数据库,数据之前双向同步,一旦挂掉快速切换到备库上。
  • 没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。
  • 非阻塞的?搞一个 while 循环,直到 insert 成功再返回成功。
  • 非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。
  • 非公平的?再建一张中间表,将等待锁的线程全记录下来,并根据创建时间排序,只有最先创建的允许获取锁。
  • 比较好的办法是在程序中生产主键进行防重。

基于表字段版本号做分布式锁

这个策略源于 mysql 的 mvcc 机制,使用这个策略其实本身没有什么问题,唯一的问题就是对数据表侵入较大,我们要为每个表设计一个版本号字段,然后写一条判断 sql 每次进行判断,增加了数据库操作的次数,在高并发的要求下,对数据库连接的开销也是无法忍受的。

基于悲观锁

基于数据库排他锁做分布式锁

在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁 (注意: InnoDB 引擎在加锁的时候,只有通过索引进行检索的时候才会使用行级锁,否则会使用表级锁。这里我们希望使用行级锁,就要给要执行的方法字段名添加索引,值得注意的是,这个索引一定要创建成唯一索引,否则会出现多个重载方法之间无法同时被访问的问题。重载方法的话建议把参数类型也加上。)。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。

我们可以认为获得排他锁的线程即可获得分布式锁,当获取到锁之后,可以执行方法的业务逻辑,执行完方法之后,通过connection.commit()操作来释放锁。

这种方法可以有效的解决上面提到的无法释放锁和阻塞锁的问题。

  • 阻塞锁? for update语句会在执行成功后立即返回,在执行失败时一直处于阻塞状态,直到成功。
  • 锁定之后服务宕机,无法释放?使用这种方式,服务宕机之后数据库会自己把锁释放掉。

但是还是无法直接解决数据库单点和可重入问题。

这里还可能存在另外一个问题,虽然我们对方法字段名使用了唯一索引,并且显示使用 for update 来使用行级锁。但是,MySQL 会对查询进行优化,即便在条件中使用了索引字段,但是否使用索引来检索数据是由 MySQL 通过判断不同执行计划的代价来决定的,如果 MySQL 认为全表扫效率更高,比如对一些很小的表,它就不会使用索引,这种情况下 InnoDB 将使用表锁,而不是行锁。如果发生这种情况就悲剧了。。。

还有一个问题,就是我们要使用排他锁来进行分布式锁的 lock,那么一个排他锁长时间不提交,就会占用数据库连接。一旦类似的连接变得多了,就可能把数据库连接池撑爆。

优缺点

优点:简单,易于理解

缺点:会有各种各样的问题(操作数据库需要一定的开销,使用数据库的行级锁并不一定靠谱,性能不靠谱)

基于 Redis 做分布式锁

基于 redis 的 setnx()、expire() 方法做分布式锁

setnx()

setnx 的含义就是 SET if Not Exists,其主要有两个参数 setnx(key, value)。该方法是原子的,如果 key 不存在,则设置当前 key 成功,返回 1;如果当前 key 已经存在,则设置当前 key 失败,返回 0。

expire()

expire 设置过期时间,要注意的是 setnx 命令不能设置 key 的超时时间,只能通过 expire() 来对 key 设置。

使用步骤

1、setnx(lockkey, 1) 如果返回 0,则说明占位失败;如果返回 1,则说明占位成功

2、expire() 命令对 lockkey 设置超时时间,为的是避免死锁问题。

3、执行完业务代码后,可以通过 delete 命令删除 key。

这个方案其实是可以解决日常工作中的需求的,但从技术方案的探讨上来说,可能还有一些可以完善的地方。比如,如果在第一步 setnx 执行成功后,在 expire() 命令执行成功前,发生了宕机的现象,那么就依然会出现死锁的问题,所以如果要对其进行完善的话,可以使用 redis 的 setnx()、get() 和 getset() 方法来实现分布式锁。

基于 redis 的 setnx()、get()、getset()方法做分布式锁

这个方案的背景主要是在 setnx() 和 expire() 的方案上针对可能存在的死锁问题,做了一些优化。

getset()

这个命令主要有两个参数 getset(key,newValue)。该方法是原子的,对 key 设置 newValue 这个值,并且返回 key 原来的旧值。假设 key 原来是不存在的,那么多次执行这个命令,会出现下边的效果:

  1. getset(key, “value1”) 返回 null 此时 key 的值会被设置为 value1
  2. getset(key, “value2”) 返回 value1 此时 key 的值会被设置为 value2
  3. 依次类推!
使用步骤
  1. setnx(lockkey, 当前时间+过期超时时间),如果返回 1,则获取锁成功;如果返回 0 则没有获取到锁,转向 2。
  2. get(lockkey) 获取值 oldExpireTime ,并将这个 value 值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向 3。
  3. 计算 newExpireTime = 当前时间+过期超时时间,然后 getset(lockkey, newExpireTime) 会返回当前 lockkey 的值currentExpireTime。
  4. 判断 currentExpireTime 与 oldExpireTime 是否相等,如果相等,说明当前 getset 设置成功,获取到了锁。如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试。
  5. 在获取到锁之后,当前线程可以开始自己的业务处理,当处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行 delete 释放锁;如果大于锁设置的超时时间,则不需要再锁进行处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import cn.com.tpig.cache.redis.RedisService;
import cn.com.tpig.utils.SpringUtils;

//redis分布式锁
public final class RedisLockUtil {

    private static final int defaultExpire = 60;

    private RedisLockUtil() {
        //
    }

    /**
     * 加锁
     * @param key redis key
     * @param expire 过期时间,单位秒
     * @return true:加锁成功,false,加锁失败
     */
    public static boolean lock(String key, int expire) {

        RedisService redisService = SpringUtils.getBean(RedisService.class);
        long status = redisService.setnx(key, "1");

        if(status == 1) {
            redisService.expire(key, expire);
            return true;
        }

        return false;
    }

    public static boolean lock(String key) {
        return lock2(key, defaultExpire);
    }

    /**
     * 加锁
     * @param key redis key
     * @param expire 过期时间,单位秒
     * @return true:加锁成功,false,加锁失败
     */
    public static boolean lock2(String key, int expire) {

        RedisService redisService = SpringUtils.getBean(RedisService.class);

        long value = System.currentTimeMillis() + expire;
        long status = redisService.setnx(key, String.valueOf(value));

        if(status == 1) {
            return true;
        }
        long oldExpireTime = Long.parseLong(redisService.get(key, "0"));
        if(oldExpireTime < System.currentTimeMillis()) {
            //超时
            long newExpireTime = System.currentTimeMillis() + expire;
            long currentExpireTime = Long.parseLong(redisService.getSet(key, String.valueOf(newExpireTime)));
            if(currentExpireTime == oldExpireTime) {
                return true;
            }
        }
        return false;
    }

    public static void unLock1(String key) {
        RedisService redisService = SpringUtils.getBean(RedisService.class);
        redisService.del(key);
    }

    public static void unLock2(String key) {    
        RedisService redisService = SpringUtils.getBean(RedisService.class);    
        long oldExpireTime = Long.parseLong(redisService.get(key, "0"));   
        if(oldExpireTime > System.currentTimeMillis()) {        
            redisService.del(key);    
        }
   }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void drawRedPacket(long userId) {
    String key = "draw.redpacket.userid:" + userId;

    boolean lock = RedisLockUtil.lock2(key, 60);
    if(lock) {
        try {
            //领取操作
        } finally {
            //释放锁
            RedisLockUtil.unLock(key);
        }
    } else {
        new RuntimeException("重复领取奖励");
    }
}

基于 Redlock 做分布式锁

Redlock 是 Redis 的作者 antirez 给出的集群模式的 Redis 分布式锁,它基于 N 个完全独立的 Redis 节点(通常情况下 N 可以设置成 5)。

算法的步骤如下:

  • 1、客户端获取当前时间,以毫秒为单位。
  • 2、客户端尝试获取 N 个节点的锁,(每个节点获取锁的方式和前面说的缓存锁一样),N 个节点以相同的 key 和 value 获取锁。客户端需要设置接口访问超时,接口超时时间需要远远小于锁超时时间,比如锁自动释放的时间是 10s,那么接口超时大概设置 5-50ms。这样可以在有 redis 节点宕机后,访问该节点时能尽快超时,而减小锁的正常使用。
  • 3、客户端计算在获得锁的时候花费了多少时间,方法是用当前时间减去在步骤一获取的时间,只有客户端获得了超过 3 个节点的锁,而且获取锁的时间小于锁的超时时间,客户端才获得了分布式锁。
  • 4、客户端获取的锁的时间为设置的锁超时时间减去步骤三计算出的获取锁花费时间。
  • 5、如果客户端获取锁失败了,客户端会依次删除所有的锁。
    使用 Redlock 算法,可以保证在挂掉最多 2 个节点的时候,分布式锁服务仍然能工作,这相比之前的数据库锁和缓存锁大大提高了可用性,由于 redis 的高效性能,分布式缓存锁性能并不比数据库锁差。

但是,有一位分布式的专家写了一篇文章《How to do distributed locking》,质疑 Redlock 的正确性。

https://mp.weixin.qq.com/s/1bPLk_VZhZ0QYNZS8LkviA

https://blog.csdn.net/jek123456/article/details/72954106

优缺点

优点:

性能高

缺点:

失效时间设置多长时间为好?如何设置的失效时间太短,方法没等执行完,锁就自动释放了,那么就会产生并发问题。如果设置的时间太长,其他获取锁的线程就可能要平白的多等一段时间。

基于 redisson 做分布式锁

redisson 是 redis 官方的分布式锁组件。GitHub 地址:https://github.com/redisson/redisson

上面的这个问题 ——> 失效时间设置多长时间为好?这个问题在 redisson 的做法是:每获得一个锁时,只设置一个很短的超时时间,同时起一个线程在每次快要到超时时间时去刷新锁的超时时间。在释放锁的同时结束这个线程。

基于 ZooKeeper 做分布式锁

zookeeper 锁相关基础知识

  • zk 一般由多个节点构成(单数),采用 zab 一致性协议。因此可以将 zk 看成一个单点结构,对其修改数据其内部自动将所有节点数据进行修改而后才提供查询服务。
  • zk 的数据以目录树的形式,每个目录称为 znode, znode 中可存储数据(一般不超过 1M),还可以在其中增加子节点。
  • 子节点有三种类型。序列化节点,每在该节点下增加一个节点自动给该节点的名称上自增。临时节点,一旦创建这个 znode 的客户端与服务器失去联系,这个 znode 也将自动删除。最后就是普通节点。
  • Watch 机制,client 可以监控每个节点的变化,当产生变化会给 client 产生一个事件。

zk 基本锁

  • 原理:利用临时节点与 watch 机制。每个锁占用一个普通节点 /lock,当需要获取锁时在 /lock 目录下创建一个临时节点,创建成功则表示获取锁成功,失败则 watch/lock 节点,有删除操作后再去争锁。临时节点好处在于当进程挂掉后能自动上锁的节点自动删除即取消锁。
  • 缺点:所有取锁失败的进程都监听父节点,很容易发生羊群效应,即当释放锁后所有等待进程一起来创建节点,并发量很大。

zk 锁优化

  • 原理:上锁改为创建临时有序节点,每个上锁的节点均能创建节点成功,只是其序号不同。只有序号最小的可以拥有锁,如果这个节点序号不是最小的则 watch 序号比本身小的前一个节点 (公平锁)。
  • 步骤:
  1. 在 /lock 节点下创建一个有序临时节点 (EPHEMERAL_SEQUENTIAL)。
  2. 判断创建的节点序号是否最小,如果是最小则获取锁成功。不是则取锁失败,然后 watch 序号比本身小的前一个节点。
  3. 当取锁失败,设置 watch 后则等待 watch 事件到来后,再次判断是否序号最小。
  4. 取锁成功则执行代码,最后释放锁(删除该节点)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;

public class DistributedLock implements Lock, Watcher{
    private ZooKeeper zk;
    private String root = "/locks";//根
    private String lockName;//竞争资源的标志
    private String waitNode;//等待前一个锁
    private String myZnode;//当前锁
    private CountDownLatch latch;//计数器
    private int sessionTimeout = 30000;
    private List<Exception> exception = new ArrayList<Exception>();

    /**
     * 创建分布式锁,使用前请确认config配置的zookeeper服务可用
     * @param config 127.0.0.1:2181
     * @param lockName 竞争资源标志,lockName中不能包含单词lock
     */
    public DistributedLock(String config, String lockName){
        this.lockName = lockName;
        // 创建一个与服务器的连接
        try {
            zk = new ZooKeeper(config, sessionTimeout, this);
            Stat stat = zk.exists(root, false);
            if(stat == null){
                // 创建根节点
                zk.create(root, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.PERSISTENT);
            }
        } catch (IOException e) {
            exception.add(e);
        } catch (KeeperException e) {
            exception.add(e);
        } catch (InterruptedException e) {
            exception.add(e);
        }
    }

    /**
     * zookeeper节点的监视器
     */
    public void process(WatchedEvent event) {
        if(this.latch != null) {
            this.latch.countDown();
        }
    }

    public void lock() {
        if(exception.size() > 0){
            throw new LockException(exception.get(0));
        }
        try {
            if(this.tryLock()){
                System.out.println("Thread " + Thread.currentThread().getId() + " " +myZnode + " get lock true");
                return;
            }
            else{
                waitForLock(waitNode, sessionTimeout);//等待锁
            }
        } catch (KeeperException e) {
            throw new LockException(e);
        } catch (InterruptedException e) {
            throw new LockException(e);
        }
    }

    public boolean tryLock() {
        try {
            String splitStr = "_lock_";
            if(lockName.contains(splitStr))
                throw new LockException("lockName can not contains \\u000B");
            //创建临时子节点
            myZnode = zk.create(root + "/" + lockName + splitStr, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.EPHEMERAL_SEQUENTIAL);
            System.out.println(myZnode + " is created ");
            //取出所有子节点
            List<String> subNodes = zk.getChildren(root, false);
            //取出所有lockName的锁
            List<String> lockObjNodes = new ArrayList<String>();
            for (String node : subNodes) {
                String _node = node.split(splitStr)[0];
                if(_node.equals(lockName)){
                    lockObjNodes.add(node);
                }
            }
            Collections.sort(lockObjNodes);
            System.out.println(myZnode + "==" + lockObjNodes.get(0));
            if(myZnode.equals(root+"/"+lockObjNodes.get(0))){
                //如果是最小的节点,则表示取得锁
                return true;
            }
            //如果不是最小的节点,找到比自己小1的节点
            String subMyZnode = myZnode.substring(myZnode.lastIndexOf("/") + 1);
            waitNode = lockObjNodes.get(Collections.binarySearch(lockObjNodes, subMyZnode) - 1);
        } catch (KeeperException e) {
            throw new LockException(e);
        } catch (InterruptedException e) {
            throw new LockException(e);
        }
        return false;
    }

    public boolean tryLock(long time, TimeUnit unit) {
        try {
            if(this.tryLock()){
                return true;
            }
            return waitForLock(waitNode,time);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    private boolean waitForLock(String lower, long waitTime) throws InterruptedException, KeeperException {
        Stat stat = zk.exists(root + "/" + lower,true);
        //判断比自己小一个数的节点是否存在,如果不存在则无需等待锁,同时注册监听
        if(stat != null){
            System.out.println("Thread " + Thread.currentThread().getId() + " waiting for " + root + "/" + lower);
            this.latch = new CountDownLatch(1);
            this.latch.await(waitTime, TimeUnit.MILLISECONDS);
            this.latch = null;
        }
        return true;
    }

    public void unlock() {
        try {
            System.out.println("unlock " + myZnode);
            zk.delete(myZnode,-1);
            myZnode = null;
            zk.close();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }
    }

    public void lockInterruptibly() throws InterruptedException {
        this.lock();
    }

    public Condition newCondition() {
        return null;
    }

    public class LockException extends RuntimeException {
        private static final long serialVersionUID = 1L;
        public LockException(String e){
            super(e);
        }
        public LockException(Exception e){
            super(e);
        }
    }
}

优缺点

优点:

有效的解决单点问题,不可重入问题,非阻塞问题以及锁无法释放的问题。实现起来较为简单。

缺点:

性能上可能并没有缓存服务那么高,因为每次在创建锁和释放锁的过程中,都要动态创建、销毁临时节点来实现锁功能。ZK 中创建和删除节点只能通过 Leader 服务器来执行,然后将数据同步到所有的 Follower 机器上。还需要对 ZK的原理有所了解。

基于 Consul 做分布式锁

DD 写过类似文章,其实主要利用 Consul 的 Key / Value 存储 API 中的 acquire 和 release 操作来实现。

文章地址:http://blog.didispace.com/spring-cloud-consul-lock-and-semphore/

使用分布式锁的注意事项

1、注意分布式锁的开销

2、注意加锁的粒度

3、加锁的方式

总结

无论你身处一个什么样的公司,最开始的工作可能都需要从最简单的做起。不要提阿里和腾讯的业务场景 qps 如何大,因为在这样的大场景中你未必能亲自参与项目,亲自参与项目未必能是核心的设计者,是核心的设计者未必能独自设计。希望大家能根据自己公司业务场景,选择适合自己项目的方案。

参考资料

http://www.hollischuang.com/archives/1716

http://www.spring4all.com/question/158

https://www.cnblogs.com/PurpleDream/p/5559352.html

http://www.cnblogs.com/PurpleDream/p/5573040.html

https://www.cnblogs.com/suolu/p/6588902.html