前言:node
随着微处理机技术的发展,人们只需花几百美圆就能买到一个CPU芯片,这个芯片每秒钟执行的指令比80年代最大的大型机的处理机每秒钟所执行的指令还多。若是你愿意付出两倍的价钱,将获得一样的CPU,但它却以更高的时钟速率运行。所以,最节约成本的办法一般是在一个系统中使用集中在一块儿的大量的廉价CPU。因此,倾向于分布式系统的主要缘由是它能够潜在地获得比单个的大型集中式系统好得多的性价比。实际上,分布式系统是经过较低廉的价格来实现类似的性能的。redis
随着互联网的兴起,愈来愈多的人使用者互联网产品。通常互联网系统都是分布式部署的,分布式部署确实能带来性能和效率上的提高,提高效率的同事,咱们还须要注意,保证一个分布式环境下数据一致性的问题。数据库
分布式锁简述性能优化
在单机时代,虽然不存在分布式锁,但也会面临资源互斥的状况,只不过在单机的状况下,若是有多个线程要同时访问某个共享资源的时候,咱们能够采用线程间加锁的机制,即当某个线程获取到这个资源后,就须要对这个资源进行加锁,当使用完资源以后,再解锁,其它线程就能够接着使用了。例如,在JAVA中,甚至专门提供了一些处理锁机制的一些API(synchronize/Lock等)。架构
可是到了分布式系统的时代,这种线程之间的锁机制,就没做用了,系统可能会有多份而且部署在不一样的机器上,这些资源已经不是在线程之间共享了,而是属于进程之间共享的资源。所以,为了解决这个问题,「分布式锁」就强势登场了。并发
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,经常须要协调他们的动做。若是不一样的系统或是同一个系统的不一样主机之间共享了一个或一组资源,那么访问这些资源的时候,每每须要互斥来防止彼此干扰来保证一致性,在这种状况下,便须要使用到分布式锁。分布式
在分布式系统中,经常须要协调他们的动做。若是不一样的系统或是同一个系统的不一样主机之间共享了一个或一组资源,那么访问这些资源的时候,每每须要互斥来防止彼此干扰来保证一致性,这个时候,便须要使用到分布式锁。微服务
分布式锁要知足哪些要求呢?高并发
目前相对主流的有三种,从实现的复杂度上来看,从上往下难度依次增长:源码分析
v 基于数据库实现
基于数据库来作分布式锁的话,一般有两种作法:
乐观锁
乐观锁的特色先进行业务操做,不到万不得已不去拿锁。即“乐观”的认为拿锁多半是会成功的,所以在进行完业务操做须要实际更新数据的最后一步再去拿一下锁就好。
乐观锁机制其实就是在数据库表中引入一个版本号(version)字段来实现的。当咱们要从数据库中读取数据的时候,同时把这个version字段也读出来,若是要对读出来的数据进行更新后写回数据库,则须要将version加1,同时将新的数据与新的version更新到数据表中,且必须在更新的时候同时检查目前数据库里version值是否是以前的那个version,若是是,则正常更新。若是不是,则更新失败,说明在这个过程当中有其它的进程去更新过数据了。
看图叙事。模拟实战场景。
如上图,故事男主人公(如下简称男主)打算去ATM机取3000元,故事女主人公(如下简称女主)则要在某宝买买买,买个包须要3000元,帐户的余额是5000元。若是没有采用锁的话,在两人同时取款和买买买,可能会出现合计消费了6000,致使帐户余额异常。因此须要用到锁的机制,当男主女主甚至更多小主同时消费时,除了读取到6000的帐户余额外,还须要读取到当前的版本号version=1,等先行消费成功的主人公(不管谁先消费)去出发修改帐户余额的同时,会触发version=version+1,即version=2。那么其余人使用未更新的version(1)去更新帐户余额时就会发现版本号不对,就会致使本次更新失败,就得从新去读取最新帐户余额以及版本号。
乐观锁遵循的两点法则:
悲观锁
悲观锁的特色是先获取锁,再进行业务操做,即“悲观”的认为获取锁是很是有可能失败的,所以要先确保获取锁成功再进行业务操做。
一般所说的“一锁二查三更新”即指的是使用悲观锁。一般来说在数据库上的悲观锁须要数据库自己提供支持,即经过经常使用的 select ... for update 操做来实现悲观锁。当数据库执行 select for update 时会获取被 select 中的数据行的行锁,所以其余并发执行的 select for update 若是试图选中同一行则会发生排斥(须要等待行锁被释放),所以达到锁的效果。 select for update 获取的行锁会在当前事务结束时自动释放,所以必须在事务中使用。
示例:
/** * 消费之后更新银行余额 * @param bankId 银行卡号 * @param cost 消费金额 * @return */ public boolean consume(Long bankId, Integer cost){ //先锁定银行帐户 BankAccount product = query("SELECT * FROM bank_account WHERE bank_id=#{bankId} FOR UPDATE", bankId); if (product.getNumber() > 0) { int updateCnt = update("UPDATE tb_product_stock SET number=#{cost} WHERE product_id=#{productId}", cost, bankId); if(updateCnt > 0){ //更新库存成功 return true; } } return false; }
乐观锁与悲观锁的区别
乐观锁的思路通常是表中增长版本字段,更新时where语句中增长版本的判断,算是一种CAS(Compare And Swep)操做,银行消费场景中version起到了版本控制的做用( AND version=#{version} )。
悲观锁之因此是悲观,在于他认为本次操做会发生并发冲突,因此一开始就对银行帐户加上锁( SELECT ... FOR UPDATE ),而后就能够安心的作判断和更新,由于这时候不会有别人更新帐户余额。
v 基于Redis实现
基于Redis实现的锁机制,主要是依赖redis自身的原子操做,例如:
SET user_key user_value NX PX 100
redis从2.6.12版本开始,SET命令才支持这些参数:
NX:只在在键不存在时,才对键进行设置操做, SET key value NX 效果等同于 SETNX key value
PX millisecond:设置键的过时时间为millisecond毫秒,当超过这个时间后,设置的键会自动失效
上述代码示例是指,当redis中不存在user_key这个键的时候,才会去设置一个user_key键,而且给这个键的值设置为 user_value,且这个键的存活时间为100ms
为何这个命令能够帮咱们实现锁机制呢?
由于这个命令是只有在某个key不存在的时候,才会执行成功。那么当多个进程同时并发的去设置同一个key的时候,就永远只会有一个进程成功。当某个进程设置成功以后,就能够去执行业务逻辑了,等业务逻辑执行完毕以后,再去进行解锁。
解锁很简单,只须要删除这个key就能够了,不过删除以前须要判断,这个key对应的value是当初本身设置的那个。
另外,针对redis集群模式的分布式锁,能够采用redis的 Redlock (可能会被墙)机制。
v 基于ZooKeeper实现
其实基于ZooKeeper,就是使用它的临时有序节点来实现的分布式锁。
原理
当某客户端要进行逻辑的加锁时,就在zookeeper上的某个指定节点的目录下,去生成一个惟一的临时有序节点, 而后判断本身是不是这些有序节点中序号最小的一个,若是是,则算是获取了锁。若是不是,则说明没有获取到锁,那么就须要在序列中找到比本身小的那个节点,并对其调用 exist() 方法,对其注册事件监听,当监听到这个节点被删除了,那就再去判断一次本身当初建立的节点是否变成了序列中最小的。若是是,则获取锁,若是不是,则重复上述步骤。
当释放锁的时候,只需将这个临时节点删除便可。
如上图,locker是一个持久节点, node_1/node_2/.../node_n 就是上面说的临时节点,由客户端client去建立的。
client_1/client_2/.../clien_n 都是想去获取锁的客户端。以client_1为例,它想去获取分布式锁,则须要跑到locker下面去建立临时节点(假如是node_1)建立完毕后,看一下本身的节点序号是不是locker下面最小的,若是是,则获取了锁。若是不是,则去找到比本身小的那个节点(假如是node_2),找到后,就监听node_2,直到node_2被删除,那么就开始再次判断本身的node_1是否是序列中最小的,若是是,则获取锁,若是还不是,则继续找一下一个节点。
总结:
分布式锁有不少种,开篇说的"相对主流的有三种"只是针对我所遇到的。分布式锁将来确定是变幻无穷的,不管你身处一个什么样的公司,最开始的工做可能都得尽量的从简单的作起。但愿你们能根据所在公司业务场景,选择适合所在项目的方案。
顺便在此给你们推荐一个Java架构方面的交流学习群:698581634,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系,主要针对Java开发人员提高本身,突破瓶颈,相信你来学习,会有提高和收获。在这个群里会有你须要的内容 朋友们请抓紧时间加入进来吧。