Q: 一个业务服务器, 一个数据库, 操作: 查询用户当前余额, 扣除当前余额的 3% 作为手续费
- synchronized
- lock
- db lock
Q: 两个业务服务器, 一个数据库, 操作: 查询用户当前余额, 扣除当前余额的 3% 作为手续费
分布式锁
我们需要怎么样的分布式锁?
可以保证在分布式部署的应用集群中, 同一个方法在同一时间只能被一台机器上的一个线程执行.
这把锁要是一把可重入锁 (避免死锁)
这把锁最好是一把阻塞锁 (根据业务需求考虑要不要这条)
这把锁最好是一把公平锁 (根据业务需求考虑要不要这条)
有高可用的获取锁和释放锁功能
获取锁和释放锁的性能要好
一, 基于数据库实现的分布式锁
基于表实现的分布式锁
- CREATE TABLE `methodLock` (
- `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
- `method_name` varchar(64) NOT NULL DEFAULT ''COMMENT'锁定的方法名',
- `desc` varchar(1024) NOT NULL DEFAULT '备注信息',
- `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间, 自动生成',
- PRIMARY KEY (`id`),
- UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';
- 1
- 2
- 3
- 4
- 5
- 6
- 7
当我们想要锁住某个方法时, 执行以下 SQL:
insert into methodLock(method_name,desc) values ('method_name','desc')
因为我们对 method_name 做了唯一性约束, 这里如果有多个请求同时提交到数据库的话, 数据库会保证只有一个操作可以成功, 那么我们就可以认为操作成功的那个线程获得了该方法的锁, 可以执行方法体内容.
当方法执行完毕之后, 想要释放锁的话, 需要执行以下 Sql:
delete from methodLock where method_name ='method_name'
上面这种简单的实现有以下几个问题:
这把锁强依赖数据库的可用性, 数据库是一个单点, 一旦数据库挂掉, 会导致业务系统不可用.
这把锁没有失效时间, 一旦解锁操作失败, 就会导致锁记录一直在数据库中, 其他线程无法再获得到锁.
这把锁只能是非阻塞的, 因为数据的 insert 操作, 一旦插入失败就会直接报错. 没有获得锁的线程并不会进入排队队列, 要想再次获得锁就要再次触发获得锁操作.
这把锁是非重入的, 同一个线程在没有释放锁之前无法再次获得该锁. 因为数据中数据已经存在了.
这把锁是非公平锁, 所有等待锁的线程凭运气去争夺锁.
当然, 我们也可以有其他方式解决上面的问题.
数据库是单点? 搞两个数据库, 数据之前双向同步. 一旦挂掉快速切换到备库上.
没有失效时间? 只要做一个定时任务, 每隔一定时间把数据库中的超时数据清理一遍.
非阻塞的? 搞一个 while 循环, 直到 insert 成功再返回成功.
非重入的? 在数据库表中加个字段, 记录当前获得锁的机器的主机信息和线程信息, 那么下次再获取锁的时候先查询数据库, 如果当前机器的主机信息和线程信息在数据库可以查到的话, 直接把锁分配给他就可以了.
非公平的? 再建一张中间表, 将等待锁的线程全记录下来, 并根据创建时间排序, 只有最先创建的允许获取锁
基于排他锁实现的分布式锁
除了可以通过增删操作数据表中的记录以外, 其实还可以借助数据中自带的锁来实现分布式的锁.
我们还用刚刚创建的那张数据库表. 可以通过数据库的排他锁来实现分布式锁. 基于 MySQL 的 InnoDB 引擎, 可以使用以下方法来实现加锁操作:
- public boolean lock(){
- connection.setAutoCommit(false);
- while(true){
- try{
- result = select * from methodLock where method_name=xxx for update;
- if(result==null){
- return true;
- }
- }catch(Exception e){
- }
- sleep(1000);
- }
- return false;
- }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
在查询语句后面增加 for update, 数据库会在查询过程中给数据库表增加排他锁. 当某条记录被加上排他锁之后, 其他线程无法再在该行记录上增加排他锁.
我们可以认为获得排它锁的线程即可获得分布式锁, 当获取到锁之后, 可以执行方法的业务逻辑, 执行完方法之后, 再通过以下方法解锁:
public void unlock(){ connection.commit(); }
通过 connection.commit(); 操作来释放锁.
这种方法可以有效的解决上面提到的无法释放锁和阻塞锁的问题.
阻塞锁? for update 语句会在执行成功后立即返回, 在执行失败时一直处于阻塞状态, 直到成功.
锁定之后服务宕机, 无法释放? 使用这种方式, 服务宕机之后数据库会自己把锁释放掉.
但是还是无法直接解决数据库单点, 可重入和公平锁的问题.
总结一下使用数据库来实现分布式锁的方式, 这两种方式都是依赖数据库的一张表, 一种是通过表中的记录的存在情况确定当前是否有锁存在, 另外一种是通过数据库的排他锁来实现分布式锁.
数据库实现分布式锁的优点
直接借助数据库, 容易理解.
数据库实现分布式锁的缺点
会有各种各样的问题, 在解决问题的过程中会使整个方案变得越来越复杂.
操作数据库需要一定的开销, 性能问题需要考虑.
二, 基于缓存的分布式锁
相比较于基于数据库实现分布式锁的方案来说, 基于缓存来实现在性能方面会表现的更好一点.
目前有很多成熟的缓存产品, 包括 Redis,Memcached 等. 这里以 Redis 为例来分析下使用缓存实现分布式锁的方案.
- public boolean trylock(String key) {
- ResultCode code = jedis.setNX(key, "This is a Lock.");
- if (ResultCode.SUCCESS.equals(code))
- return true;
- else
- return false;
- }
- public boolean unlock(String key){
- ldbTairManager.invalid(NAMESPACE, key);
- }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
- try {
- return interProcessMutex.acquire(timeout, unit);
- } catch (Exception e) {
- e.printStackTrace();
- }
- return true;
- }
- public boolean unlock() {
- try {
- interProcessMutex.release();
- } catch (Throwable e) {
- log.error(e.getMessage(), e);
- } finally {
- executorService.schedule(new Cleaner(client, path), delayTimeForClean, TimeUnit.MILLISECONDS);
- }
- return true;
- }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
来源: http://www.bubuko.com/infodetail-2977457.html