【有书共读10】《码农翻身》读书笔记第二章

从session到token:

一个简单HTTP协议, 就是请求加响应,  尤其是我不用记住是谁刚刚发了HTTP请求,   每个请求都是全新的!而30年前的Web 基本上就是文档的浏览而已, 既然是浏览,作为一个服务器, 为什么要记住谁在一段时间里都浏览了什么文档呢?

但是好日子没持续多久, 很快大家就不满足于静态的Html 文档了, 交互式的Web应用开始兴起, 尤其是论坛, 在线购物等网站。我马上就遇到了和邮件服务器一样的问题, 那就是必须管理会话,必须记住哪些人登录系统,  哪些人往自己的购物车中放了商品,  也就是说我必须把每个人区分开。这对我来说是个不小的挑战, 由于HTTP协议的无状态特性, 我必须加点小手段,才能完成会话管理。我想出的办法就是给大家发一个会话标识(session id), 说白了就是一个随机的字符串,每个人收到的都不一样,  每次大家向我发起HTTP请求的时候,把这个字符串给一并捎过来, 这样我就能区分开谁是谁了。

大家都很高兴, 可是我就不爽了。每个人只需要保存自己的session id,而我需要保存所有人的session id !  如果访问我的人多了, 就得由成千上万,甚至几十万个。这对我来说是一个巨大的开销 , 严重的限制了我的扩展能力, 比如说我用两个机器组成了一个集群, 小F通过机器A登录了系统,  那session id会保存在机器A上,  假设小F的下一次请求被转发到机器B怎么办?  机器B可没有小F的 session id啊。有时候我会采用一点小伎俩: session sticky , 就是让小F的请求一直粘连在机器A上, 但是这也不管用, 要是机器A挂掉了, 还得转到机器B去。

那我只好做session 的复制了, 把session id  在两个机器之间搬来搬去, 快累死了。


后来有个叫Mem***d的给我支了招: 把session id 集中存储到一个地方, 所有的机器都来访问这个地方的数据, 这样一来,就不用复制了, 但是增加了单点失败的可能性, 要是那个负责session 的机器挂了,  所有人都得重新登录一遍, 估计得被人骂死。


我也尝试把这个单点的机器也搞出集群,增加可靠性, 但不管如何, 这小小的session 对我来说是一个沉重的负担。

这几天的晚上我一直在思考, 我为什么要保存这可恶的session呢, 只让每个客户端去保存该多好?

可是如果我不保存这些session id ,  我怎么验证客户端发给我的session id 的确是我生成的呢?  如果我不去验证,我都不知道他们是不是合法登录的用户, 那些不怀好意的家伙们就可以伪造session id , 为所欲为了。

嗯,对了,关键点就是验证 !比如说, 小F已经登录了系统, 我给他发一个令牌(token), 里边包含了小F的 user id, 下一次小F 再次通过Http 请求访问我的时候, 把这个token 通过Http header 带过来不就可以了。

不过这和session id没有本质区别啊, 任何人都可以可以伪造,  所以我得想点儿办法, 让别人伪造不了。

那就对数据做一个签名吧, 比如说我用HMAC-SHA256 算法,加上一个只有我才知道的密钥,  对数据做一个签名, 把这个签名和数据一起作为token ,   由于密钥别人不知道, 就无法伪造token了。



这个token 我不保存,  当小F把这个token 给我发过来的时候,我再用同样的HMAC-SHA256 算法和同样的密钥,对数据再计算一次签名, 和token 中的签名做个比较, 如果相同, 我就知道小F已经登录过了,并且可以直接取到小F的user id ,  如果不相同, 数据部分肯定被人篡改过, 我就告诉发送者: 对不起,没有认证。



Token 中的数据是明文保存的(虽然我会用Base64做下编码, 但那不是加密), 还是可以被别人看到的, 所以我不能在其中保存像密码这样的敏感信息。

当然, 如果一个人的token 被别人偷走了, 那我也没办法, 我也会认为小偷就是合法用户, 这其实和一个人的session id 被别人偷走是一样的。

这样一来, 我就不保存session id 了, 我只是生成token , 然后验证token ,  我用我的CPU计算时间获取了我的session 存储空间 !

解除了session id这个负担,  可以说是无事一身轻, 我的机器集群现在可以轻松地做水平扩展, 用户访问量增大, 直接加机器就行。   这种无状态的感觉实在是太好了!


XML

诞生和发展

我虽然20岁不到, 可已经是满脸沧桑了, 这不能怪我, 你们人类的IT技术发展的实在太快。很多技术别说20岁了, 10岁,甚至5岁都活不到就销声匿迹了。

20年来, 我有着波澜壮阔的人生, 曾经在多个领域占据***地位, 但如今这些领域已经被侵蚀得不成样子了。

我的诞生可以算是一个分水岭,在此之前,程序之间想交换数据, 都用自定义的格式, 千奇百怪,无所不有。


攻城掠地

除了数据传输以外, 我最早在配置文件领域取得了成功, 说道配置文件, 之前都是简单的key=value 这样的属性文件, 描述简单的结构还行, 对于复杂的结构就力不从心了,  我XML就不同了, 全称是‘Extensible Markup Language’ , 可扩展性极强,标签随意定制,即使是在复杂的逻辑下,我XML也是得心应手,我给你看个web.xml的例子吧:


人民群众的眼睛是雪亮的, 看到我的可扩展性这么强悍,想定义啥tag就定义啥tag , 纷纷从属性文件倒戈, 什么Struts , Spring ,Hibernate , MyBatis 写配置文件时不约而同地选择了我。

这是个巨大的胜利啊, 除了这个之外,我还成功地进入了新的领域, 还是先看个例子:


这个代码片段中<copy>是表示把文件从一个地方复制到另外一个地方。 <zip>是把文件打成zip包 , 是不是一目了然?

这其实是Ant的一段代码, 你一看就明白是怎么回事了, 所以我非常适合描述任务和流程, 因为其中有复杂的控制,其实你想想,Ant是用我的标签定义了一套自动化处理的‘语言’出来, 程序员可以用这个‘语言’ 写出非常复杂的程序出来, 厉害吧?


蚕食

好景不长, 俗话说物极必反,盛极必衰。  在数据传输和数据交换这个领域, 很快被Javascript 和 JSON 撕开了一个口子, 他们充分地利用了人们对冗余标签的不满, 搞出了这样简洁的格式:

简洁,数据量小, 方便传输, 更重要的是Javascript 解析起来非常方便。

我说你们这是乱搞, 没有schema ,  你这数据怎么验证啊?出错了怎么办?

他们告诉我, 老先生,您想多了,我们用JSON做同一个应用内的数据传输, 从浏览器到服务器, 或者从App到服务器, 同一个公司的人, 只要接口定义好,大部分情况下都没问题,不用费心做验证, 即使数据被损坏,抛出错误就行了啊。我表示无语, 只能眼睁睁地看着JSON攻城掠地, 把数据传输这一块高地慢慢地蚕食了。再说说配置文件吧, 原来喜欢我的码农开始抱怨, 他们说XML描述又臭又长,说简短的配置文件还可以, 稍微长一点看起来头都晕了。怎么办?  Spring, Hibernate 这帮家伙见风使舵, 纷纷转向Java注解。XML文件配置能干的活, Java注解也可以做, 甚至干的更好, 于是我又流失了一大批拥趸。

SOA也好景不长,逐渐没落,有个叫Roy Thomas Fielding的家伙 发表了一篇论文, 提了一个叫做RESTful风格的Web架构出来, 搞得人心浮动, RESTful风格的服务开始大行其道, 使用传统Web 服务的人越来越少, 既然没人用Web 服务, 也就没人使用XML来写WSDL,   于是这块阵地也慢慢的丢失了。屈指一算, 我丢失了三块重要的阵地, 很快从巅峰滑落了。不是我不明白,是技术变化实在快,现在唯一能安慰我的就是在复杂的结构、任务、流程的描述方面, 我还有着不可替代的作用。


加锁还是不加锁

互斥锁

虽说锁是个好东西, 但是我们线程日常使用的都是互斥锁, 所谓互斥,就是同一时刻只有获得锁的那个线程才有资格去操作共享资源,  别的线程都阻塞住了,被放到了一个叫锁池(Lock pool)的地方,什么事情都干不了。

比如说这个简单的Sequence类吧, 有100个线程拼命地挤破头去进入next()方法, 但由于synchronized的存在, 大家必须得获得一把锁才可以, 隔壁的小明运气不错, 获得了操作系统的垂青, 喜滋滋的得到了宝贵的锁, 进入了next()方法去做事了。而我们剩下的99个线程大眼瞪小眼, 除了叹口气,感慨下人生之不如意十之***, 还能干嘛?老老实实地进入锁池里待着去吧!等到隔壁小明做完了事情, 美滋滋的拿着最新的value值出来以后, 我们这99个在锁池里吹牛的线程一跃而起,去竞争那个刚刚被释放的锁。但是下一个幸运儿是谁呢?  不知道?有时候人类为了公平,会搞个队列让我们排好队,先进先出。 但是我已经活了4.998秒,人生快走到了尽头, 在这么长的人生里, 我体会到的真理是: 公平实在是个稀缺货,不公平才是常态!所以年轻人不要老是抱怨这个社会, 没用的, 还是老老实实的奋斗吧。不要加锁?平淡的日子就这么过着, 有一天线程世界来了一个年轻人,自称为小李,  他看着我们这么努力地奋斗着去争抢那把锁, 不由地嘲笑道: 你们真傻啊, 难道不知道不加锁也能做事吗?我们愣了一下,人群中立刻发出一阵爆笑:哈哈哈, 这小子疯了,没有锁岂不又回到互相覆盖数据的日子了!小李不甘示弱:你们这帮土老帽,把元老院的那帮老家伙的话当做圣旨, 岂不知天外有天, 人外有人, 这世界大得很呐!这句话把我们镇住了, 我小心翼翼地问: 那你说说,不加锁怎么才能保证正确性呢?“就拿你们的那个Sequence类来说吧, 不就是并发的更新内存中的一个值吗, 可以这么分为三步来做:

1. 从内存中读取value 值,假设为10, 我们把这个值称为A

2. B = A+1 得到 B = 11

3. 用A 和 内存的值相比, 如果相等(就是说在过去的一段时间,没人修改内存的值), 那就把B的值(11)写入内存,  如果不相等(就是说过去的一段时间, 有人修改了内存value 的值), 意味着A已经不是最新值了, 那就放弃这次修改, 跳回第1步去”

我们面面相觑, 就这么简单? 真的没有加锁啊。隔壁的小明反应最快: 小李子, 你这第三步有问题啊, 你看需要读内存吧,需要比较吧,还得写入内存吧, 这不是一个原子操作,  在我们多线程并发执行的时候, 肯定会出问题!

小李说: “唉, 说你们老土吧, 你们还不服气, 听说过comare and swap 这个硬件指令没有?  那个第三步其实就是一条硬件指令,保证原子执行。 在单个CPU上就不用说了,如果是有多个CPU, 这个指令甚至会锁住总线, 确保同一时刻只有一个CPU能访问内存!这样吧, 干脆写成个指令:  compareAndSwap(内存的值, A , B) , 这下子明白了吧?  还不明白? 估计是人类的语言你们听起来不太明白, 来吧,给你们来点熟悉的代码:”

看到了我们熟悉的代码, 我的脑海飞速盘算:假定我和小明都同时进入了这段代码, 都读到了内存的值A  = 10 ,  然后小明的时间片到了,只好退出CPU,  我则愉快的继续执行。对于我来说 A = 10 , B = 11,  然后我运行compareAndSwap ,我发现我的A值和内存值是相等的,于是就把新的值B写入内存, 并且返回,退出next 函数, 收工回家。等到小明再次被运行的时候, 由于他的初始值A也是10 , 他也得到B = 11, 当他运行compareAndSwap 就发现A的值和内存不相等了(因为我改成了11) , 那小明只好再次循环,获得A = 11, B = 12 , 再次调用compareAndSwap , 如果还是被别人抢了先, 小明只好再次循环, 从内存获得A = 12 , B =13 ....   直到成功为止。想到小明一直循环下去,累得要死的样子, 我”***“地笑了。我抬起头,正好和小明的目光相遇, 看到他不怀好意的样子, 估计也是把我置于无限循环的想象中了。



Java中的CAS
小李说: “Compare And Swap 这个词太长了, 以后简称CAS,希望你们听得懂。”小明问道: “我们是Java 语言, 你那个读取内存的值该怎么办, 还有那个compareAndSwap 函数,我们实现不了啊?”小李说:“你们Java 不是有JNI(Java native interface)吗?  可以用C语言来实现, 然后在Java中封装一下不就得了?”

“看看这个AtomicInteger,  他就代表了那个内存的值, 那个count.compareAndSet方法只有两个参数, 实际上内存的值隐藏在了AtomicInteger当中,你们Java 不是喜欢面向对象嘛!”我们仔细地审视这段代码, 它根本没有加锁, 每个人都可以进入next()方法, 读取数据,操作数据, 最后使用CAS来决定这次操作是否有效, 如果内存值被别人改过,那就再次循环尝试。小李总结到: “你们之前的synchronized 叫做悲观锁, 元老院太悲观了,总是怕你们把事情搞砸,你看现在乐观一点儿, 不也玩的挺好嘛! 每个线程都不用阻塞,不用在那个无聊的锁池里待着。 要知道,阻塞,激活是一笔不小的开销啊。”



CAS的扩展

使用非阻塞算法的线程越来越多, 小李趁热打铁,提供了一系列所谓Atomic的类:

AtomicBoolean

AtomicInteger

AtomicLong

AtomicIntegerArray

AtomicLongArray

这些工具类都很好用, 大家非常喜欢, 只是我们发现小李的这些工具类只支持简单的类型,对于一些复杂的数据结构,就不好使用CAS了,因为使用CAS需要频繁的读写内存数据,并且做数据的比较,  如果数据结构很复杂, 那读写内存是不可承受之重,还不如最早的悲观锁呢!小李胸有成竹, 马上给出了改进: 不要比较数据啊, 只比较引用不就得了, 这里有一个AtomicReference, 拿去用吧。我们向元老院做了推荐, 那些老家伙们可真是有两把刷子, 立刻提出了一个我做梦都没有想到的问题:假设有两个线程, 线程1 读到内存的数值为 A ,  然后时间片到期,撤出CPU。 线程2运行,线程2 也读到了A , 把它改成了B, 然后又把B改成原来的值A , 简单点说,修改的次序是 A -> B ->A  。 然后线程1开始运行, 它发现内存的值还是A , 完全不知道内存中已经***作过。

(注: 这就是著名的ABA问题)

我想了一下, 好像没什么啊,不就是把数字改成了原来的值吗?也没什么影响。可是小李却陷入了沉思, 看来这是一个挺难的问题, 他口中念念有词: 如果只是简单的数字,那没什么, 可是如果使用AtomicReference, 并且操作的是复杂的数据结构,就可能会出问题了。对了, 我可以用一个版本号来处理啊, 给每个放入AtomicReference的对象都加入一个version, 这样以来尽管值相同, 也能区分开了! 嗯, 我就叫他AtomicStampedReference 吧。元老院很满意, 但是还是发了一个公告:  鉴于最近使用AtomicXXXX的线程越来越多, 元老院有责任提醒各位, 用这些类实现非阻塞算法是非常容易出错的,在你自己实现之前, 看看元老院有没有提供现成的类,例如: ConcurrentLinkedQueue。 如果非要自己写,也得提交给元老院审查通过才可以使用。



Doug Lea

如果说要从Java 世界中找一个并发编程的大牛, 我想这个人非Doug Lea莫属, 从JDK 1.5开始, Java 引入了一个非常著名的线程并发库java.util.concurrent , 由于其良好的抽象, 这个库极大的降低了并发编程的难度, 其作者就是并发编程的权威Doug Lea, 他是纽约州立大学Oswego分校计算机科学系教授, JCP(Java Community Process)执行***会成员,JSR166(并发编程)的*** , 文中的小李就是向Doug Lea致敬。






CAP定理

计算机界有很多高大上又难于理解的术语,CAP就是其中之一, 什么一致性(Consistency), 可用性(Availability), 分区容错性(Partition tolerance) 就很难理解了,  再加上CAP定理更是让人云里雾里,  今天咱们试图通俗的演绎一下。


                  展示分布式系统是怎么工作的:



虽然只是两台机器, 但是也构成了一个简单的分布式环境。


有一天, 用户先访问了左边的机器A , 写入了一条数据,  然后机器A很不幸, 网线被悲催的网管给踢掉了, 这直接导致了两个严重的后果:

1. 负载均衡找不着机器A,认为它死翘翘了, 就要把用户的下一次访问转到机器B去。

2. 数据复制也找不着机器A  ,  只好罢工。 用户刚写入的数据没法复制到机器B,机器B上还是老数据

怎么办?   虽然这是一次偶然, 把网管臭骂一顿, 插上网线就可以了, 但是谁能保证以后两个机器的通信是一致畅通的呢?组里的小王说:    我们的机器B 还活着呢, 还能提供服务, 数据复制不到机器B, 不就是少看几条数据嘛, 无伤大雅,不影响大局, 勉强可用, 插上网线后数据复制就会工作, 一切就会恢复正常。小王无意中选择了系统的可用性(Availability,简称A), 系统能提供服务就好, 数据不一致可以忍受。张大胖说:  不行,  老板说了,我们系统的数据极为重要, 数据如果不一致会带来严重后果,所以机器B上的和这些关键数据相关的功能也必须停掉, 必须等到机器A插上网线,数据同步以后才能开工

很明显, 张大胖遵循老板指示, 把一致性(Consistency, 简称C )放到了首位。所以问题就很明显了, 在网络节点之间无法通信的情况下,  和数据复制相关的功能, 要么选择可用性(A) , 要么选择一致性(C), 不能同时选择两者。大胖仔细思考了一下, 其实这两种选择的背后其实隐藏着另外一个事实, 那就是网络节点之间无法通信的情况下, 节点被隔离,产生了网络分区,  整个系统仍然是可以工作的, 大胖给它起了个名: 分区容错性(Partition tolerance, 简称P)。如果选择了可用性(A) + 分区容错性(P) ,  就要放弃一致性(C)。如果选在一致性(C) + 分区容错性(P) , 就得放弃可用性(A)  ,   对了, 这种情况下,虽然系统的有些功能是不能使用的, 因为需要等待数据的同步, 但是那些和数据同步无关的功能还是可以访问的 , 相当于系统做了功能的降级。既然有AP和CP,    会不会出现仅仅是CA(一致性+可用性)这种组合呢? 就是没有分区容错性, 只保留可用性和一致性? 仔细想想, 这种情况其实就退化成了单机应用, 没有意义了。大胖觉得自己似乎发现了一个规律:   在一个分布式计算机系统中,一致性(C),可用性(A)和分区容错性(P) 这三种保证无法同时得到满足,最多满足两个。他决定把找个规律叫做CAP定理, 听起来比较高大上, 显得自己高深莫测。如果你实在是搞不懂这CAP,   张大胖会告诉你一个更容易理解的版本: 在一个分布式系统中, 在出现节点之间无法通信(网络分区产生), 你只能选择 可用性 或者 一致性,  没法同时选择他们。

  


单元测试

1.  单元测试是“白盒测试”, 应该覆盖各个分支流程、异常条件

2.  单元测试面向的是一个单元("Unit"), 是java中的一个类或者几个类组成的单元。

3.  单元测试运行一定要快!

4.  单元测试一定是可重复执行的

5.  单元测试之间不能有相互依赖,应该是独立的。

6.  单元测试代码和业务代码同等重要, 要一并维护。




#笔记##读书笔记#
全部评论

相关推荐

3 3 评论
分享
牛客网
牛客企业服务