侧边栏壁纸
博主头像
博主等级

  • 累计撰写 19 篇文章
  • 累计创建 34 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

分布式锁使用总结

前尘一梦
2022-06-14 / 0 评论 / 0 点赞 / 30 阅读 / 2506 字

前言

分布式锁一般用来解决存在并发执行的业务场景中资源争抢的问题,实现有多种方式,如数据库,Zookeeper, 分布式缓存,日常项目中以Redis来实现的锁使用比较多,以下记录一些使用过程中的心得。

心得

分布式锁的三要素

  • 加锁和设置过期时间两个操作必须为原子性,否则当加锁成功而设置过期时间的命令执行失败,解锁的代码也没执行成功,就会造成死锁。

  • 加锁和解锁的必须为同一个线程,如果其他线程把当前线程的锁给解了,就会导致业务问题。

  • 锁需要有续期机制,如果当前线程任务执行时间比较长且还没完成,而锁已经过期了,就会造成其他线程也能拿到锁,造成业务问题,有些伙伴会想着把它设置得久一点,但是这个多长时间算久是不好权衡的,正确的方式是使用异步线程在后台实现一个锁监听, 每隔一段时间检查当前当前锁是否仍被持有,如果有的话,给锁做一个续期,比如 Redisson 的默认 Watchdog 时间是 30 秒。即锁的初始过期时间是 30 秒,如果业务逻辑执行时间超过 30 秒,Watchdog 会每隔 10 秒续约一次,将锁的过期时间延长到 30 秒。如果业务执行完毕后已解锁,则不会再对其进行续期。

以Redis实现的分布式锁

  • Lua脚本

  • Redisson

  • 基于 Redlock 算法的分布式锁

第一种方式属于常规实现,在执行很快,不存在业务阻塞的情况下可正常使用,但如果某些接口需要处理复杂业务,执行时间具有不确定性,则可能会存在问题。

第二种方式较第一种方式增加了锁续期机制,适合用于复杂业务场景,使用也简单,Redisson封装了完整易用的API,项目中引入其依赖并注入相关配置即可使用,这也是平时使用比较多,且推荐使用的方式。

private RedissonClient redissonClient;

public boolean tryLock(String lockKey) {
    RLock lock = redissonClient.getLock(lockKey);
    return lock.tryLock();
}

第二种方式相对来说比较完善,但是也存在单节点故障问题,所以有了第三种方式Redlock算法的实现,一次加锁需要加在多个 Redis 实例上,只有过半成功才算加锁成功,通过同步机制提高可靠性和容错性。但是因为跨网络通信,可能会带来额外的网络延迟和开销,所以用在性能要求不高,但需要极高可靠性、高可用性、跨数据中心部署的重要业务场景下,这种方式据我了解目前使用较少,在机器,网络资源有保障的情况下,Redis集群一般都比较稳定, 而且加锁集群和业务依赖集群一般是同一个,如果整个缓存服务都挂了,那其他业务估计也会受到很大影响,单单加锁服务可用也没啥意义,除非加锁和业务使用的集群也分开。

常见加锁场景

  • 业务对象具有唯一属性并且不能同时处于多个并发操作中,比如在数字藏品领域,每一份藏品都对应一个藏品编码,多个低阶藏品可用于合成一个高阶藏品,如果在高并发场景,用户通过脚本调用接口合成时,不对每一份藏品编码进行一个锁定,就很容易会出现一码多用的情况。

  • 用户行为锁定,比如某些商品限制同一个用户只能购买一个,此时我们很容易的想到使用数据库唯一键来解决这个问题,但是用户过多无效的操作其实是会空耗我们的服务资源,应该在程序入口就进行拦截,此时粗暴一点可以直接以用户id为key,让用户同一时间只能在系统内做同一件事,细粒度则可以以用户id加业务标识为key加锁。

注意点

  • 使用Redisson加锁的时候不能显示设置过期时间,不然内置Watchdog机制会不生效

  • 在有事务的场景,一定要在事务方法外加锁,解锁,不然会出现事务还未提交,锁已经释放掉了的情况, 此时可以在controller层加锁,或者在事务方法外层再包一层方法,如果加锁的地方比较多,可以考虑基于AOP加自定义注解实现统一处理加锁,解锁。

总结

以上简单记录了一些分布式锁的使用心得,在一些和金钱交易紧密相关的业务场景下,很容易出现一些专业黑产通过脚本等进行不正当交易,这时候锁的使用就相当重要了。

0

评论区