分布式锁的实现基于数据库悲观锁、redis、zookeeper

分布式锁

在同一个jvm进程内,我们可以使用synchronized或者ReentrantLock锁,来完成对共享资源的互斥访问。然而现在大多数系统都是分布式系统,jvm进程分布在不同的节点上,为了全局数据的一致性,这个时候就需要分布式锁了。
下面展示几种分布式锁的实现

  1. 数据库,使用悲观锁for update机制。
  2. Zookeeper,基于瞬时有序节点的特性
  3. Redis,基于Setnx实现分布式锁

数据库悲观锁

原理

通过select …for update访问同一条数据

实现

  1. 在数据库中建立分布式锁所需的表
CREATE TABLE `distributed_lock` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `business_code` varchar(50) NOT NULL COMMENT '业务代码',
  `describe` varchar(255) NOT NULL COMMENT '描述',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='分布式锁';
-- 模拟订单业务,添加数据
INSERT INTO `test_1`.`distributed_lock` (`id`, `business_code`, `describe`) VALUES (1, 'order', '订单锁');
  1. 在项目中添加for update语句,我这里演示是用的mybatis
    /**
     * 通过业务代码获取锁
     * @param businessCode  业务代码
     * @return
     */
    @Select("select * from distribute_lock where business_code = #{businessCode} for update")
    DistributeLock selectDistributeLock(@Param("businessCode") String businessCode);
  1. controller测试用例
    /**
     * 数据库分布式锁
     * 一定要添加事务@Transactional,否则for update执行完毕后会自动释放锁
     */
    @RequestMapping("dbLock")
    @Transactional(rollbackFor = Exception.class)
    public void dbLock() throws Exception {
        log.info("我进入了dbLock方法!");
        // 调用for update语句
        DistributeLock distributeLock = distributeLockMapper.selectDistributeLock("order");
        // 如果不为空则进入锁,为空代表已经有其它请求占住了锁
        if (distributeLock == null) {
            throw new Exception("分布式锁找不到");
        }
        log.info("我进入了锁!");
        try {
            // 模拟业务用时
            Thread.sleep(20000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("执行完毕,释放锁!");
    }

redis分布式锁

原理

基于Redis的Setnx实现分布式锁,利用NX的原子性。多个线程并发时,只有一个线程可以设置成功,设置成功即获得锁,可以执行后续的业务处理,如果出现异常,锁过期会自动释放。释放锁采用Redis的delete命令。释放锁时要校验之前设置的value,相同才可释放,防止释放了别人的锁,释放锁采用LUA脚本。
set [key] [value] NX PX 30000

  • key:资源名称,可根据不同业务区分不同锁
  • value:唯一值,用于释放锁时的校验
  • NX :key不存在时设置成功,key存在时设置不成功
  • PX:自动失效时间,出现异常情况,锁可以过期失效

实现

  1. 编写redisLock的封装
/**
 * 实现AutoCloseable接口来自动释放锁
 */
@Slf4j
public class RedisLock implements AutoCloseable {

    private RedisTemplate redisTemplate;
    private String key;
    private String value;
    //单位:秒
    private int expireTime;

    public RedisLock(RedisTemplate redisTemplate,String key,int expireTime){
        this.redisTemplate = redisTemplate;
        this.key = key;
        this.expireTime=expireTime;
        //使用UUID作为value
        this.value = UUID.randomUUID().toString();
    }

    /**
     * 获取分布式锁
     */
    public boolean getLock(){
        RedisCallback<Boolean> redisCallback = connection -> {
            //设置NX
            RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
            //设置过期时间
            Expiration expiration = Expiration.seconds(expireTime);
            //序列化key
            byte[] redisKey = redisTemplate.getKeySerializer().serialize(key);
            //序列化value
            byte[] redisValue = redisTemplate.getValueSerializer().serialize(value);
            //执行setnx操作
            return connection.set(redisKey, redisValue, expiration, setOption);
        };
        //获取分布式锁
        return (Boolean)redisTemplate.execute(redisCallback);
    }

    /**
     * 释放锁
     */
    public boolean unLock() {
        String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
                "    return redis.call(\"del\",KEYS[1])\n" +
                "else\n" +
                "    return 0\n" +
                "end";
        RedisScript<Boolean> redisScript = RedisScript.of(script,Boolean.class);
        List<String> keys = Collections.singletonList(key);

        return (Boolean)redisTemplate.execute(redisScript, keys, value);
    }


    /**
     * 自动释放锁
     */
    @Override
    public void close() throws Exception {
        unLock();
    }
}
  1. controller测试用例
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * redis锁
     */
    @RequestMapping("redisLock")
    public void redisLock() {
        log.info("我进入了redisLock方法!");
        // 使用自动释放锁
        try (RedisLock redisLock = new RedisLock(redisTemplate, "order", 30)) {
            // 获取锁
            if (redisLock.getLock()) {
                log.info("我进入了锁!!");
                //模拟业务
                Thread.sleep(20000);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        log.info("执行完毕,自动释放锁!");
    }

zookeeper分布式锁

原理

基于zookeeper的瞬时有序节点的特性,多线程并发创建瞬时节点时,得到有序的序列,序号最小的线程获得锁。其他的线程监听自己序号的前一个序号,以此类推。
观察器:
1. 节点数据发生变化,发生给客户端
2. 观察器只能监控一次,再监控需要重新设置

实现

  1. 编写zkLock
/**
 * 实现AutoCloseable接口来自动释放锁
 * 实现Watcher接口来使用观察器
 */
@Slf4j
public class ZkLock implements Watcher, AutoCloseable {

    private ZooKeeper zooKeeper;
    private String businessCode;
    private String znode;

    public ZkLock(String connectString, String businessCode) throws IOException {
        this.zooKeeper = new ZooKeeper(connectString, 30000, this);
        this.businessCode = businessCode;
    }

    /**
     * 获取锁
     */
    public boolean getLock() throws KeeperException, InterruptedException {
        // 判断业务根节点是否存在 /order
        Stat existsNode = zooKeeper.exists("/" + businessCode, false);
        if (existsNode == null) {
            //不存在创建业务根节点,业务节点要设置为持久节点,是所有的锁要放在哪个节点下。
            zooKeeper.create("/" + businessCode, businessCode.getBytes(),
                    ZooDefs.Ids.OPEN_ACL_UNSAFE, // 无需账号密码即可连接
                    CreateMode.PERSISTENT);      // 创建持久节点
        }

        // 创建瞬时有序节点 /order/order_ ,zooKeeper会在_后自动加序号的
        znode = zooKeeper.create("/" + businessCode + "/" + businessCode + "_", businessCode.getBytes(),
                ZooDefs.Ids.OPEN_ACL_UNSAFE,
                CreateMode.EPHEMERAL_SEQUENTIAL); // 创建瞬时有序节点

        // 拿到当前创建的节点的名称
        znode = znode.substring(znode.lastIndexOf("/") + 1);

        // 获取 /order 下所有的子节点
        List<String> childrenNodes = zooKeeper.getChildren("/" + businessCode, false);
        // 对所有子节点排序
        Collections.sort(childrenNodes);
        // 第一个子节点
        String firstNode = childrenNodes.get(0);

        // 如果是第一个节点,则获得锁
        if (!znode.equals(firstNode)) {
            return true;
        }

        // 不是第一个节点则监听前一个节点
        // 默认第一个节点为前一个节点
        String lastNode = firstNode;
        for (String node : childrenNodes) {
            // 找到当前节点,对前一个节点进行监听
            if (znode.equals(node)) {
                zooKeeper.exists("/" + businessCode + "/" + lastNode, true);
                break;
            } else {
                // 未找到设置当前节点为前一个节点
                lastNode = node;
            }
        }
        // 监听的时候开启线程等待
        synchronized (this) {
            wait();
        }
        return true;
    }

    /**
     * 监听器
     */
    @Override
    public void process(WatchedEvent watchedEvent) {
        //如果节点被删除,唤起线程
        if (watchedEvent.getType() == Event.EventType.NodeDeleted) {
            synchronized (this) {
                notify();
            }
        }
    }

    /**
     * 释放锁
     */
    @Override
    public void close() throws Exception {
        zooKeeper.delete("/" + businessCode + "/" + znode, -1);
        zooKeeper.close();
    }
}
  1. controller测试用例
    /**
     * zookeeper锁
     */
    @RequestMapping("zkLock")
    public void zkLock() {
        log.info("我进入了zkLock方法!");
        // 使用自动释放锁
        try (ZkLock zkLock = new ZkLock("localhost:2181", "order")) {
            // 获取锁
            if (zkLock.getLock()) {
                log.info("我进入了锁!!");
                //模拟业务
                Thread.sleep(20000);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        log.info("执行完毕,自动释放锁!");
    }

优缺点

方式优点缺点
数据库实现简单、易于理解对数据库压力大
Redis易于理解不支持阻塞
Zookeeper支持阻塞要理解Zookeeper,编写复杂
Curator提供锁的方法依赖Zookeeper,强一致
Redisson提供锁的方法,可以支持阻塞

总结

  • 可以自己写代码研究,但项目中不推荐自己编写分布式锁
  • 推荐使用Redisson和Curator实现的分布式锁
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

赛赛liangks

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值