介绍
许多场景中,数据一致性是一个比较重要的话题,在单机环境中,我们可以通过Java提供的并发API来解决;而在分布式环境(会遇到网络故障、消息重复、消息丢失等各种问题)下要复杂得多,比如电商的库存扣减,秒杀活动,集群定时任务执行等需要进程互斥的场景。本文主要探讨如何利用Zookeeper来实现分布式锁,对比了一些其他方案分布式锁的优缺点。至于使用何种,要因自己的业务场景去决定,没有绝对的方案。
分布式锁是什么?
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。
实现分布式锁注意点:
锁的可重入性(递归调用不应该被阻塞、避免死锁)
锁的超时(避免死锁、死循环等意外情况)
锁的阻塞(保证原子性等)
锁的特性支持(阻塞锁、可重入锁、公平锁、联锁、信号量、读写锁)
使用分布式锁注意点:
分布式锁的开销(分布式锁一般能不用就不用,有些场景可以用乐观锁代替)
加锁的粒度(控制加锁的粒度,可以优化系统的性能)
加锁的方式
常见的实现分布式锁方案
数据库
基于数据库表唯一索引
最简单的方式就是直接创建一张锁表,当我们要锁住某个方法或资源时,我们就在该表中增加一条记录,想要释放锁的时候就删除这条记录。给某字段添加唯一性约束,如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。
但会引入数据库单点、无失效时间、不阻塞、不可重入等问题。
基于数据库排他锁
如果使用的是MySql的InnoDB引擎,在查询语句后面增加for update,数据库会在查询过程中(须通过唯一索引查询)给数据库表增加排他锁,我们可以认为获得排它锁的线程即可获得分布式锁,通过 connection.commit() 操作来释放锁。
会引入数据库单点、不可重入、无法保证一定使用行锁、排他锁,所以有可能长时间不提交导致占用数据库连接等问题。
优缺点
优点:
直接借助数据库,容易理解。
缺点
会引入更多的问题,使整个方案变得越来越复杂
操作数据库需要一定的开销,有一定的性能问题
使用数据库的行级锁并不一定靠谱,尤其是当我们的锁表并不大的时候
缓存
相比较于基于数据库实现分布式锁的方案来说,基于缓存来实现在性能方面会表现的更好一点,目前有很多成熟的缓存产品,包括Redis、memcached、tair等。
基于 redis 的 setnx()、expire() 方法做分布式锁
setnx 的含义就是 SET if Not Exists,其主要有两个参数 setnx(key, value)。该方法是原子的,如果 key 不存在,则设置当前 key 成功,返回 1;如果当前 key 已经存在,则设置当前 key 失败,返回 0。
expire 设置过期时间,要注意的是 setnx 命令不能设置 key 的超时时间,只能通过 expire() 来对 key 设置。