一、为什么要用到分布锁
1、多环境中才需要
2、任务都需要对同一共享资源时空行写操作
3、对资源访问互斥
锁竞争4个步骤:
1、竞争锁
2、占有锁
3、任务阻赛
4、释放锁
二、分布式锁几种方案及比较
在实现zk锁前,先简单说明一下模版方法模式
在父类中编排主流程,将步骤实现延迟到子类去实现。
比如下图:
在模版类中定义清点商品,计算价目,支付,送货上门
主流程方法,并实现了清点商品,计算价目,送货上门方法,但在子中实现不同支付方法。
public abstract class AbstractTemplate {
public void goumai(){
shangPing();
jiShuan();
pay();
fahuo();
}
public void shangPing(){
System.out.println("清点商品");
}
public void jiShuan(){
System.out.println("计算");
}
public void fahuo(){
System.out.println("发货");
}
public abstract void pay();
}
public class WeiPayTemplate extends AbstractTemplate {
@Override
public void pay() {
System.out.println("weipay");
}
public static void main(String[] args){
WeiPayTemplate w = new WeiPayTemplate();
w.goumai();
}
}
那么我们实现锁也是这个过成程,在抽象类中定义获取锁,竞争锁,等待锁,释放锁
定义一lock类,Mysql redis zk子类都都继承这个抽象类具体在子类实现各自的方法。
1 、 mysql锁
新建一张表
method_Lock
CREATE TABLE `method_Lock` (
`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='锁定中的方法';
当我们想要锁住某个方法时,执行以下SQL:
insert into method_Lock (method_name,desc) values (‘method_name’,‘desc’)
如果新增成功那么,获取锁成功,失败那么获取锁失败。
当方法执行完毕之后,想要释放锁的话,需要执行以下Sql:
delete from method_Lock where method_name ='method_name'
模版方法:
public abstract class AbstractLock implements Lock {
public void getLock(){
//1 、竞争锁
if(tryLock()){
System.out.print("##get lock锁的资源###");
}else{
//2、任务阻塞
waitLock();
//重新获取锁
getLock();
}
}
public abstract void waitLock();
public abstract void unLock();
public abstract boolean tryLock();
}
mysql锁实现类
@Service
public class MysqlLock extends AbstractLock {
private static final int LOCK_ID = 1;
@Autowired
private LockMapper lockMapper;
@Override
//非阻塞式加锁
public boolean tryLock() {
try {
lockMapper.insert(LOCK_ID);
}catch (Exception e){
return false;
}
return true;
}
//让当前线程休眠一段时间
@Override
public void waitLock() {
try{
Thread.currentThread().sleep(10);
}catch (InterruptedException e){
e.printStackTrace();
}
}
//释放锁
@Override
public void unLock() {
lockMapper.delete(LOCK_ID);
}
}
调用
public class MysqlTest extends AbstratApplicationBaseBootTest {
private static int i = 1;
@Resource
private MysqlLock mysqlLock;
public void getNum(){
try{
mysqlLock.getLock();
Date d = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("当前时间:" + sdf.format(d));
String date = sdf.format(d);
System.out.println(Thread.currentThread().getName()+"生成订单号"+date+" "+i);
i = i+1;
}finally {
mysqlLock.unLock();
}
}
@Test
public void test() throws InterruptedException {
System.out.println("生成唯一订单号");
for(int i =1;i<=20;i++){
new Thread(new Runnable() {
@Override
public void run() {
getNum();
}
}).start();
}
Thread.sleep(10000);
}
}
结果:生成20个不同的订单号
##get lock锁的资源###当前时间:2019-10-13 01:51:42
Thread-28生成订单号2019-10-13 01:51:42 1
[2019-10-13 01:51:42,921] [Thread-15] [INFO ] org.springframework.beans.factory.xml.XmlBeanDefinitionReader 316 -- Loading XML bean definitions from class path resource [org/springframework/jdbc/support/sql-error-codes.xml]
[2019-10-13 01:51:42,975] [Thread-15] [INFO ] org.springframework.jdbc.support.SQLErrorCodesFactory 128 -- SQLErrorCodes loaded: [DB2, Derby, H2, HDB, HSQL, Informix, MS-SQL, MySQL, Oracle, PostgreSQL, Sybase]
##get lock锁的资源###当前时间:2019-10-13 01:51:43
Thread-22生成订单号2019-10-13 01:51:43 2
##get lock锁的资源###当前时间:2019-10-13 01:51:43
Thread-18生成订单号2019-10-13 01:51:43 3
##get lock锁的资源###当前时间:2019-10-13 01:51:43
Thread-24生成订单号2019-10-13 01:51:43 4
##get lock锁的资源###当前时间:2019-10-13 01:51:43
Thread-23生成订单号2019-10-13 01:51:43 5
##get lock锁的资源###当前时间:2019-10-13 01:51:43
Thread-16生成订单号2019-10-13 01:51:43 6
##get lock锁的资源###当前时间:2019-10-13 01:51:44
Thread-27生成订单号2019-10-13 01:51:44 7
##get lock锁的资源###当前时间:2019-10-13 01:51:44
Thread-30生成订单号2019-10-13 01:51:44 8
##get lock锁的资源###当前时间:2019-10-13 01:51:44
Thread-31生成订单号2019-10-13 01:51:44 9
##get lock锁的资源###当前时间:2019-10-13 01:51:44
Thread-33生成订单号2019-10-13 01:51:44 10
##get lock锁的资源###当前时间:2019-10-13 01:51:44
Thread-15生成订单号2019-10-13 01:51:44 11
##get lock锁的资源###当前时间:2019-10-13 01:51:44
Thread-19生成订单号2019-10-13 01:51:44 12
##get lock锁的资源###当前时间:2019-10-13 01:51:45
Thread-20生成订单号2019-10-13 01:51:45 13
##get lock锁的资源###当前时间:2019-10-13 01:51:45
Thread-25生成订单号2019-10-13 01:51:45 14
##get lock锁的资源###当前时间:2019-10-13 01:51:45
Thread-32生成订单号2019-10-13 01:51:45 15
##get lock锁的资源###当前时间:2019-10-13 01:51:45
Thread-34生成订单号2019-10-13 01:51:45 16
##get lock锁的资源###当前时间:2019-10-13 01:51:45
Thread-26生成订单号2019-10-13 01:51:45 17
##get lock锁的资源###当前时间:2019-10-13 01:51:45
Thread-29生成订单号2019-10-13 01:51:45 18
##get lock锁的资源###当前时间:2019-10-13 01:51:45
Thread-21生成订单号2019-10-13 01:51:45 19
##get lock锁的资源###当前时间:2019-10-13 01:51:46
Thread-17生成订单号2019-10-13 01:51:46 20
1.1、利用mysql实现的锁缺点:
性能差,最多并发量700~1000 ,差点的机器可以才100~200并发,满足不了大的并发需求
死锁,如果一个线程获取锁后,程序执行中断,那么锁没有释放,就会死锁
不优雅,查看wailLock这段代码,是休眠一段时间后再去获锁,没法在锁释放后自动监听锁被释放,而需要休眠一段时间 不断去查看锁是否被释放掉。
非阻塞的?需要自己搞一个while循环,直到insert成功再返回成功。或通过递归重复获取锁,直到成功才返回
这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。
我们也可以有其他方式解决上面的问题。
- 数据库是单点?搞两个数据库,数据之前双向同步。一旦挂掉快速切换到备库上。
- 没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。
- 非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了
1.2、基于数据库排他锁
除了可以通过增删操作数据表中的记录以外,其实还可以借助数据中自带的锁来实现分布式的锁。
我们还用刚刚创建的那张数据库表。可以通过数据库的排他锁来实现分布式锁。 基于MySql的InnoDB引擎,可以使用以下方法来实现加锁操作:
public boolean lock(){
connection.setAutoCommit(false)
while(true){
try{
result = select * from method_Lock where method_name=xxx for update;
if(result==null){
return true;
}
}catch(Exception e){
}
sleep(1000);
}
return false;
}
在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁(这里再多提一句,InnoDB引擎在加锁的时候,只有通过索引进行检索的时候才会使用行级锁,否则会使用表级锁。这里我们希望使用行级锁,就要给method_name添加索引,值得注意的是,这个索引一定要创建成唯一索引,否则会出现多个重载方法之间无法同时被访问的问题。重载方法的话建议把参数类型也加上。)。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。
我们可以认为获得排它锁的线程即可获得分布式锁,当获取到锁之后,可以执行方法的业务逻辑,执行完方法之后,再通过以下方法解锁:
public void unlock(){
connection.commit();
}
通过connection.commit()操作来释放锁。
这种方法可以有效的解决上面提到的无法释放锁和阻塞锁的问题。
- 阻塞锁? for update语句会在执行成功后立即返回,在执行失败时一直处于阻塞状态,直到成功。
- 锁定之后服务宕机,无法释放?使用这种方式,服务宕机之后数据库会自己把锁释放掉。
但是还是无法直接解决数据库单点和可重入问题。
这里还可能存在另外一个问题,虽然我们对method_name 使用了唯一索引,并且显示使用for update来使用行级锁。但是,MySql会对查询进行优化,即便在条件中使用了索引字段,但是否使用索引来检索数据是由 MySQL 通过判断不同执行计划的代价来决定的,如果 MySQL 认为全表扫效率更高,比如对一些很小的表,它就不会使用索引,这种情况下 InnoDB 将使用表锁,而不是行锁。如果发生这种情况就悲剧了。。。
还有一个问题,就是我们要使用排他锁来进行分布式锁的lock,那么一个排他锁长时间不提交,就会占用数据库连接。一旦类似的连接变得多了,就可能把数据库连接池撑爆
1.3、总结
总结一下使用数据库来实现分布式锁的方式,这两种方式都是依赖数据库的一张表,一种是通过表中的记录的存在情况确定当前是否有锁存在,另外一种是通过数据库的排他锁来实现分布式锁。
数据库实现分布式锁的优点
直接借助数据库,容易理解。
数据库实现分布式锁的缺点
会有各种各样的问题,在解决问题的过程中会使整个方案变得越来越复杂。
操作数据库需要一定的开销,性能问题需要考虑。
使用数据库的行级锁并不一定靠谱,尤其是当我们的锁表并不大的时候。
2 、zk锁
先了解一下zk
zk是一个Nosql的数据库 有监听机制
zk 与redis类似 都是 no sql
k v结构的数据
创建一个节点命令:
create /deer 1
如果再次执上面那个命令,会执行失败,那么我们就是利用zk这一特性来实现锁的
2.1、下面是zk原生客户端通过临时节点实现一种分布式锁如下:
public abstract class ZookeeperAbstractLock extends AbstractLock {
private static final String CONNECTSTRING = "127.0.0.1:2181";
protected ZkClient zkClient = new ZkClient(CONNECTSTRING);
protected static final String PATH = "/lock";
protected static final String PATH2 = "/lock2";
}
public class ZookeeperLock extends ZookeeperAbstractLock {
private CountDownLatch countDownLatch = null;
@Override
public void waitLock() {
//创建节点删除监听事件
IZkDataListener iZkDataListener = new IZkDataListener() {
@Override
public void handleDataChange(String s, Object o) throws Exception {
}
@Override
public void handleDataDeleted(String s) throws Exception {
if(countDownLatch != null){
countDownLatch.countDown();
}
}
};
//注册事件
zkClient.subscribeDataChanges(PATH,iZkDataListener);
//如果节点存在
if(zkClient.exists(PATH)){
countDownLatch = new CountDownLatch(1);
try{
countDownLatch.await();
}catch (Exception e){
}
}
//删除监听
zkClient.unsubscribeDataChanges(PATH,iZkDataListener);
}
@Override
public void unLock() {
if(zkClient!=null){
zkClient.delete(PATH);
zkClient.close();
System.out.print("释放资源");
}
}
@Override
public boolean tryLock() {
try{
//创建临时节点
zkClient.createEphemeral(PATH);
return true;
}catch (Exception e){
return false;
}
}
}
运行
public class OrderService {
private AbstractLock lock = new ZookeeperLock();
private static int i = 1;
public void getNumber(){
try{
lock.getLock();
Date d = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("当前时间:" + sdf.format(d));
String date = sdf.format(d);
System.out.println(Thread.currentThread().getName()+"生成订单号"+date+" "+i);
i = i+1;
}finally {
lock.unLock();
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("生成唯一订单号");
for(int i =1;i<=20;i++){
new Thread(new Runnable() {
@Override
public void run() {
new OrderService().getNumber();
}
}).start();
}
Thread.sleep(10000);
}
}
zk分布锁问题:
会出现死锁问题:通过临时节点可以解决死锁问题。
临时节点特性:在创建节点后,如果程序突中断,那么节点会被自动删除,就不会出现死锁情况。
zk共4种节点类型
1、持久 不会被删除 可以有子节点
2、临时 运行一半,客户端和服务器断开连接 那么节点会被自动删除 不能有子节点
3、临时带序号
4、持久带序号
2.2 基于Curator这个开源框架来实现zk分布式锁
基于Curator这个开源框架来实现zk分布式锁,实际上就是利用临时带序号节点实现的
可查看另一篇文章:5-zk实战,zk应用
2.3、带序号临时节点实现流程图
3、基于缓存锁 reids
基于缓存锁redis目前也有开源框架redisson,可设置锁过期时间 防止死锁,可watch dog机制实现锁续期,可重入等。具体实现可以查看我的另一篇文章:官方redis实现分布式锁算法-RedLock
三、三种方案的比较
上面几种方式,哪种方式都无法做到完美。就像CAP一样,在复杂性、可靠性、性能等方面无法同时满足,所以,根据不同的应用场景选择最适合自己的才是王道。
从理解的难易程度角度(从低到高)
数据库 > 缓存 > Zookeeper
从实现的复杂性角度(从低到高)
Zookeeper >= 缓存 > 数据库
从性能角度(从高到低)
缓存 > Zookeeper >= 数据库
从可靠性角度(从高到低)
Zookeeper > 缓存 > 数据库
redis
redis简单使用
存1T数据怎么存?
用集群
配置一个读写分离? slave of
redis主节点有几个子节点 用什么命令: info replication
哨兵 RESP redis底层通信协议
resp手写一个jedis分表分库 手动一个读写分离
redis重启后,数据会丢失吗? 持久化 2种机制:RDB AOF 2种
redis怎么实现分布锁
setnx 命令
redis怎么实现购物车? hash
redis怎么实现优惠券功能? 30过期,发布订阅监听机制,监听失效后 修改数据库中状态
数据库
mysql优化
建表语句 一些索引
写一些sql语句,这些sql是否高效 ,是否用到索引?
index: namd age pos
select * from staffs where name = 1; 1是int ,会有隐式转换,所以索引失效
没有指定列不会用到索引
explain
sql 语句 查看执行计划 ,查看key是否有值 ,有值用到索引,没值没用到索引
select * from staffs where age = 1; 没有用到索引
最佳左前缀原则: