Tomcat的锁Tomcat是这个系统的核心组件。每当有用户请求时,Tomcat就会从线程池中找一个线程来处理。查看购物车,有的下订单,看着下属辛勤工作,完成人的请求,Tomcat很有成就感。同时也很自豪的是,所有的业务逻辑都在掌控之中。什么是MySQL!不就是一个保存数据的地方吗?什么是Redis!不就是个缓存来提速吗?没有他们,我可以找到替代品,我是不可替代的,Tomcat经常这么想。昨天MySQL无意中提到隔壁机器入驻了一个叫Node.js的家伙。它实际上只使用一个线程来执行JavaScript代码和实现各种业务逻辑。JavaScript也能来后台?打回来?这不是废话吗?不过要小心,别让他把生意都抢光了。想到这里,Tomcat立即去查看各个线程的情况,是否有人故意偷懒。线程0x9527和0x7954又吵起来了,原因很简单,都是做减库存的操作:读取库存,修改库存,写回数据库。线程并发执行导致三个操作交织,***数据不一致。Tomcat说:“你干什么?为什么要读出库存?你就不能直接更新库存吗?让MySQL老哥保证正确性,你要学会甩锅!”0x7954回复:“不行,张大发的代码是这样写的,好像是商家要求的,在扣除库存之前,先看看库存是否够用。”Tomcat牙疼,忍不住想到了Redis的处理方式,对于每一个读写缓存的请求,Redis都会将它们排队,组队后用一个线程一个一个处理,肯定没有这样的并发问题。但是我这里做不到。访问数据库是一个极其缓慢的操作。如果只用一个线程一个一个地处理请求,所有的请求都得等待,人会匆匆死去。没办法,Tomcat丢给两人一个Java对象:“这是一把锁。谁先抢到谁就可以进行扣库存这三个操作。以及何时执行它。张胖子感觉分布式锁有点问题。为什么这几天程序执行很慢?他以为是机器性能不够,就申请了好几台新机器,装了好几台Tomcat,组成一个集群,这下可好了。共有三个Tomcat。每个Tomcat都有一把锁来控制对库存的访问。在TomcatJVM进程内部,只有一个luckythread可以同时扣库存,但是现在是三个Tomcat。幸运的是三个人。这三个幸运儿在扣库存的时候,还是出现0x7954、0x9527这样的错误,但是现在他们互不认识,连吵架的机会都没有。三位Tomcat觉得头大,在这个分布式环境下,有多个进程在运行,进程中原来的锁已经过期了,当务之急是找一个客观公正独立的第三方来实现锁功能。MySQL提议:“到我这里来找一把锁!”“能提供锁服务吗?公开给我们用?”雄猫A问道。“不不不,不是锁服务,我给你一张数据库表,这张表lock_name的字段是有唯一约束的。”“你是说,我们的线程每次要获取锁,就去数据库插入一条数据?”TomcatA迅速响应。insertintolocks(lock_name,...)values('stock',...);"是的,我的唯一性约束只能保证一个成功,其他都失败,相当于获取了一把锁。当然,运行后那个线程执行完了,需要释放锁。”deletefromlockswherelock_name='stock'是一个简单的方法,但也是一个重量级的方法:每拿到一个锁,就得访问一次数据库!假设TomcatA的0x9527在前面,插入了一条数据,获得了锁。那来自于TomcatB的0x7954操作一定失败了,此时0x7954应该怎么办呢?能不能阻塞等待TomcatB把他唤醒?不会,因为连TomcatB都不知道0x9527操作什么时候完成,除非MySQL通知每一个Tomcat,这肯定是不可能的。那么0x7954@TomcatB只能做一件事:等一会,然后再试!这个循环一直持续到获得锁为止。但是如果0x9527获取到锁,TomcatA在执行过程中挂掉了,那么数据库记录会一直存在,不会有人删除,锁也永远不会释放!过期未释放的锁还得找清洁工清理,实在是太麻烦了。Redis这个时候Redis说:“别上MySQL的贼船!他的方法太麻烦了,不就是找第三方保存锁信息吗?用我的缓存多好啊!”!”是内存,速度会快很多!”TomcatB说道。我不需要在这里这么麻烦。你可以尝试在我的缓存中为Tomcat线程设置一个值,比如stock_lock=true。谁先设置成功,谁拿到锁就可以扣除库存。”“如果有多个线程设置,你能保证只有一个成功,其他都失败吗?”Redis拍了拍胸膛:“绝对保证!”(码农翻了老刘的注:其实是setnx命令)MySQL撇撇嘴:“和我的方案本质上是一样的,Tomcat的线程修改inventory后,还是要unlock删除stock_lock。”Redis说:“我这里也可以设置过期时间。如果TomcatA上的线程获取了锁,然后TomcatA挂了,到了到期时间,我可以自动删除stock_lock,其他线程可以重新获取锁!”“嗯,更高级,更快”三个Tomcat都同意了。定时自动释放的问题“等一下,这个自动删除过期锁有问题!”MySQL突然反驳道。“什么问题?Redis没想到数据库老哥要反抗。”假设TomcatA上0x9527获取锁执行扣库存操作,然后因为某种原因被阻塞,阻塞时间超过过期时间,则锁是你释放的,到头来还是会出现不一致!”“你挑剔,绝对是小概率事件!”Redis喊道!,道理是一样的。”行锁第二天,MySQL找到Tomcat好开心:“兄弟们,昨晚和Quartz(一个知名的定时执行框架)聊了很久,他告诉我一种新的分布式实现方式与数据库锁。解决方案是行锁。""看你没有,通过加一个forudpate,这条SQL语句会锁住这一行,也就是你获取到了锁!一旦事务提交,行锁就会自动释放。”“其他没有获取到锁的线程呢?”“自然是阻塞了。当其他线程释放行锁时,它可以自动获取它。代码中不需要循环重试。你看,以前的解决办法都是做不到的。”MySQL说。“那如果有一个线程长时间不释放行锁,会发生什么情况呢?Tomcat最关心的就是这个。”然后其他线程会等待,占用数据库连接不释放它。好吧,如果连接占用太多了,连接池就会出问题……”MySQL信心不足,这是致命的问题。”哈哈,看你傻眼了!还是用我的锁吧!”Redis笑道。“那为什么可以用Quartz呢?”MySQL不死心。“估计Quartz业务单一,锁释放快,不会有问题。”CAS就在这时候,Node.js悄悄的走了过来,拉开了数据库老头:“前辈,别给他们讲常识,不就是扣库存吗,用的什么分布式锁!,我们做吧this:"#old_num=先获取现有存货数量#new_num=#old_num-10updasterocksetstock_num=#new_numwhereproduct_id=#product_idandstock_num=#old_numMySQL亮了,是的,每次传入这个#old_num作为条件调用update语句,如果成功,说明这段时间没有其他线程更新库存;如果不成功,则重新执行这三个语句,直到成功。几天后,Tomcat等人也听说了这个方案,惊讶地说:“这不就是我们Java中常用的CompareAndSet(CAS)吗?”进程内锁本质上是一样的。1、互斥一次只能由一台机器上的一个线程获取。2.***支持阻塞再唤醒,让那些等待的线程不需要在循环中重试。3.***可重入(本文不涉及,见《编程世界的那把锁》)4.获取和释放锁的速度更快5.对于分布式锁,需要找一个集中的“地方”(数据库,Redis,Zookeeper等)来保存锁,这个地方肯定是高可用的。6.考虑到“不可靠”的分布式环境,分布式锁需要设置一个过期时间7.CAS的思想很重要。【本文为专栏作家“刘欣”原创稿件,转载请通过作者微信获取授权公众号编码】点此查看该作者更多好文
