1. 前言
网上看到一个网友的评论:现在本科应届毕业生都要求会分布式了吗?
真的太卷了!!!学不动了
听这个概念也很久了,而且其实我对分布式的学习也没那么深入…可是面试又要问,害!
先说说这个东西吧,本来单体应用情况下,我们经常说线程安全,于是出现了锁和同步的一系列操作(synchronized啊,Lock啊,JUC啊之类的),数据库方面也出现了事务的隔离性。现在问题是分布式场景,集群环境,实际上开了多个进程,那么java的这些并发相关api就无能为力了,只能借助一些第三方的工具,所有的服务器都通过这个第三方来获取锁。
这就叫分布式锁,目的是为了保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行
目前分布式锁的三种主流实现方式
基于数据库
基于redis
基于zookeeper
目的:
可以保证在分布式集群下线程安全,同一个方法在同一时间只能被一台机器上的一个线程执行。
这把锁要是一把可重入锁(避免死锁:同一个线程同一次事务访问同一行两次,拿不到锁)
有高可用的获取锁和释放锁功能(加锁失败能重加,解锁失败能让锁过期)
获取锁和释放锁的性能要好
2. 基于数据库的分布式锁
2.1 基于表(乐观锁)
实现:
就是记录一张lock表,加锁:添加一条记录,有当前线程名和锁定的方法名。解锁:删除这条记录。
注意事项:
- 针对线程安全问题:锁定的方法名这一个字段当然要unique,否则失去意义(只能一个线程执行一个方法)
- 针对可重入问题:请求锁时除了要判断该方法是否已经存在以外,还要判断是否就是本线程,是则直接return true
好处和问题:
数据库挂掉:需要多个数据库备份和同步
针对加锁失败:insert插入失败,没有获得锁,就直接回滚事务不能后续,解决办法是while循环一直请求到成功才return
针对解锁失败:delete解锁可能失败,没有失效时间,导致记录一直在数据库中,之恶能定时任务清理该表
2.2 基于排他锁(悲观锁)
实现:
表和上面一样,加锁:connection.setAutoCommit(false);开启事务但不提交,加锁语句为select * from lock where method_name = xx for update。解锁:connection.commit();
注意事项:
- 针对线程安全问题:for update是mysql的innodb一种行锁排他锁实现,因此一定要引擎支持行锁,且必须加唯一索引才会走行锁,否则锁表
- 针对可重入问题:同上
好处和问题:
- 不存在加锁失败问题:因为for update失败会阻塞直到成功
- 不存在解锁失败问题:因为锁并不是插入的数据,而是事务,出错了会回滚,数据库会把锁释放掉
- 性能问题:有时候sql优化器优化了,表太小时不走索引,导致锁表,影响其他方法的进行
- 性能问题:事务长时间不提交的性能问题,同时占用连接池
2.3 总结
容易理解,性能有问题,考虑的多了写起来越来越复杂。不推荐
2. 基于redis的分布式锁
实现:
这个最简单,就是一条记录key: 方法名 value: 当前线程
注意事项
针对加锁失败问题:要while循环,同上
针对解锁失败问题:要记得设置过期时间
针对可重入问题:同上,判断是否本身
问题:
- 解锁失败可以通过过期时间来完成:但是过期时间设置多少呢?少了可能事务没完成就过期了,线程不安全。多了的话虽然安全,但是如果真的解锁失败,要白等很久
总结:
redis可以简单的部署集群,而且支持过期时间,解决了可用性的问题,而且性能也好,问题就在于过期时间的设置
3. 基于zookeeper的分布式锁
实现:
虽然我没用过zookeeper,但是这个点还是看过,基于临时有序节点,加锁:添加有序节点,解锁:删除有序节点。可以直接使用zookeeper第三方库Curator客户端,这个客户端中封装了一个可重入的锁服务。
总结:
针对线程安全问题:没问题,zookeeper本身就是为了一致性
针对可重入问题:没问题,创建节点的时候,把当前客户端的主机信息和线程信息直接写入到节点中,下次想要获取锁的时候和当前最小的节点中的数据比对一下就可以了。如果不一样就再创建一个临时的顺序节点,参与排队。
针对加锁失败:没问题,本身就是阻塞的,客户端可以检查自己创建的节点是不是当前所有节点中序号最小的,如果是,那么自己就获取到锁
针对解锁失败:没问题,临时节点在session断开时自动删除
本文参考:
https://www.cnblogs.com/austinspark-jessylu/p/8043726.html
redis部分的实现参考: