分布式锁实现的几种方式

分布式锁实现的几种方式

写在前面:本篇文章主要搬自本人的云笔记,不当之处,欢迎指正,共同学习;

1、数据库锁的方式

1.1、基于表记录的实现方式

准备条件:创建表ABC,并做唯一索引version;
//1、线程A加锁:
    insert into ABC(version)values (versionID);
//2、线程A释放锁:
    delete from ABC where version =versionID;
如果数据库中有该锁记录,线程B再保存数据进行加锁就会发生唯一索引异常,这时就成功实现了分布式锁;
缺点:只适用于并发量不大的业务场景,这里xxl-job的管理后台上使用了这种方式;
    容易发生死锁,但是可以起一个定时任务去清理它;

1.2、基于乐观锁的实现方式

乐观锁主要根据加版本号的方式实现,原来如下:A、B线程执行更新操作之前先查询版本号,然后根据版本号进行更新,如果版本号被线程A更新了,
    这时线程B再用该版本号执行更新就会更新失败,从而成功实现加锁;这里需要注意此种锁为不可重入锁,只能重新发起业务请求;

//1、查询出数据库中的版本号
    select ID,version from ABC;//获取到versionID
//2、对该条记录进行更新
    update ABC set business = business-1 , version = versionID+1 where version=versionID and id =ID;
这时如果线程B再用原来的versionID进行更新,就失败了;
缺点:需要维护版本字段,造成数据库表结构的冗余;
    只适用于并发不高,写操作少的情况;

1.3、基于悲观锁的实现方式

//1、由于MySQL是默认自动事务提交的,这里需要先关闭事务自动提交
    SET AUTOCOMMIT = 0;(有没人知道使用spring事务的时候,怎么不关闭自动提交呢?)
//2、对于InnoDB的数据表来说,支持表级锁和行级锁,当查询时不走索引的时候加的就是表级锁;对于MYISAM(不支持事务)的数据表,只支持表级锁;
    select * from ABC where a.version =versionID for update;//行级锁
    select * from ABC where a.version >versionID for update;//表级锁
    select * from ABC where a.business =businessID for update;//表级锁
//3、读锁属于共享锁,A线程对该条记录上共享锁后,B线程也可以上共享锁,但不能上排它锁;
    写锁属于排它锁,A线程对该条记录上排它锁后,B线程不能再上任何锁;
//4、这里多补充一点Oracel中有for update nowait语法,使用该语法不会像for update当该行被锁定时一直等待,而是直接报错;
    MySQL中可以是通过配置 innodb_lock_wait_timeout的时间指定该查询会话等待的时间,超时就直接报错

2、基于Redis实现分布式锁

优点:redis是单线程执行的,所以它天然的支持分布式锁
缺点:超时时间的设置不当,会对服务性能有较大影响(我们的项目设置的8s)

实现代码如下:

//1、加锁基于set key value [EX seconds|PX milliseconds] [nx|xx];
    EX 表示超时时间精度是秒
    PX 表示超时时间精度是毫秒
    NX 表示只有当该key 不存在时才保存
    xx 表示只有当该key存在时才覆盖
public class RedisTool {

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {

        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }
}
2、解锁使用Lua脚本,保证操作的原子性
public class RedisTool {

    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {

        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }

}

3、基于zookeeper实现分布式锁

3.1、排他锁的原理,主要应用zk上到临时节点

1、多个client共同去zk上预定义好的/lock节点下创建一个相同的临时节点/abc;
2、哪个节点创建成功之后,就说明该会话拿到了锁;
3、其他创建失败的client,继续保持对该节点的监听;
4、当之前拿到锁的client会话结束或者主动释放了锁,这时其他的client再次去/lock下创建/abc,以便获取锁

缺点:每一次只有一个client注册成功,其他的client都会失败,然后触发监听时还要通知每一个client,再次去竞争锁,这里如果请求量大的话,会引发羊群效应,这里可以尝试使用共享锁

3.2、共享锁的原理,主要应用zk上的临时有序节点

1、当多个请求到来时,同样去zk上预定义好的/lock节点下创建节点,但是是临时有序节点/abc0000001,这样每个请求都可以创建成功
2、当创建成功后,并不是每个节点都能够拿到锁,只有当前有序节点之前没有其他节点,即当前节点的序号是最小的时候,才说明了该节点拿到了锁;
3、拿到锁的请求,会话结束后,删除掉对应的临时节点即可;
4、其他没有拿到的锁的请求只需要保持对它前一个节点的监听,当它前面的节点不存在了,自己变成了最小的序号了,这就说明自己拿到了锁

未完待续。。。。。。

总结

整理不宜,转载请指明出处,共同进步吧!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值