分布锁-mysql,redis,zk

一、为什么要用到分布锁

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; 没有用到索引
最佳左前缀原则:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值