redis分布式锁

为什么使用分布式锁

  • 因为随着业务的增大,用户数量的增加,为了满足业务的高效性,集群出现了,导致Java API中简单的锁机制,比如Synchronized、lock等,已经不能够满足协调多个应用之间的共享资源了,于是就出现了分布式锁。
  • 分布式锁可以协调集群中的多个应用对共享资源的获取,可以说它是一种约束和规则。

分布式锁需要具备什么特点(条件)

  • 锁的互斥性:在分布式集群应用中,共享资源的锁在同一时间只能被一个对象获取。
  • 锁要可重入,允许重复加锁。这样可以避免死锁,因为获取锁之后如果需要再次获取时发现不能获取了,这样会造成死锁。
  • 支持锁超时,防止死锁。
  • 阻塞、公平:可以根据业务需求,考虑是使用阻塞、还是非阻塞,公平还是非公平的锁。
  • 能够高效的加锁和解锁,获取锁和释放锁的性能要好。

三种分布式锁

一、数据库分布式锁

  • 数据库分布式锁的实现分为悲观锁和乐观锁。

    数据库悲观锁的方式实现分布式锁(基于SQL语句来实现)
    • 悲观锁的实现依赖于数据库自身的锁机制。它通过 select…from table where…for update 来加排它锁,当某条记录被加上排他锁之后,其他线程就无法再在该行记录上增加排他锁了;通过 connection.commit()方法来释放锁。缺点较多,不推荐。
    • (注意: InnoDB 引擎在加锁的时候,只有通过索引进行检索时才会使用行级锁,否则会使用表级锁。这里我们希望使用行级锁,就要给执行的字段添加索引)(值得注意的是,这个索引一定要创建成唯一索引,否则会出现多个重载方法之间无法同时被访问的问题)
    • 缺点:
      • 可重入问题。
      • 数据库加锁和解锁的操作较为复杂,而且操作数据库的开销较大。
      • 虽然我们对字段使用了索引,并且使用 for update 来使用行级锁。但是,MySQL 会对查询进行优化,即便在条件中使用了索引字段,但是否使用索引来检索数据是由 MySQL 通过判断不同执行计划的代价来决定的,如果 MySQL 认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引了,这种情况下 InnoDB 将使用表锁,而不是行锁。
      • 使用排他锁来进行分布式锁的加锁,那么如果排他锁长时间不提交,就会占用数据库连接。一旦类似的连接变多了,就可能把数据库连接池撑爆。
    数据库乐观锁的方式实现分布式锁(基于版本号来实现)
    • 基于版本号的方式来实现,类似于CAS机制,它在表中添加一个版本号的字段,当更新版本号失败时,客户端就进行重试,重新读取最新的版本号再次尝试更新。

二、Redis实现分布式锁

  • 简单来说就是在Redis里占个位置,当别的线程也要来占时,发现已经被占了,只能等待
  • 它主要有三个命令:
    • setnx key val 。设置分布式锁。setnx 的含义就是 SET if Not Exists,该方法是原子的,如果 key 不存在,则设置当前 key 成功,返回 1;如果当前 key 已经存在,则设置当前 key 失败,返回 0。
    • expire key timeout 。为key设置过期时间,单位是秒,超过这个时间,锁会自动释放,可以避免死锁。但如果执行expire失败,可能会出现死锁的问题,可以通过将setnx和expire一起使用来解决:set lock_key lock_value nx px timeout
    • SET key value [EX seconds] [PX millisecounds] [NX|XX]
      EX seconds:设置键的过期时间为second秒
      PX millisecounds:设置键的过期时间为millisecounds 毫秒
      NX:只在键不存在的时候,才对键进行设置操作
      XX:只在键已经存在的时候,才对键进行设置操作
      SET操作成功后,返回的是OK,失败返回NIL
    • delete key 删除key。
  • 具体原理是:获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间自动释放锁,锁的value值为一个随机生成的UUID,通过这个value值,在释放锁的时候进行判断。获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。释放锁的时候,通过UUID判断是不是当前持有的锁,若是该锁,则执行delete进行锁释放。

优化 redis 分布式锁(基于 setnx()、get()、getset() 方法做分布式锁)

主要是在 setnx() 和 expire() 的方案上针对可能存在的死锁问题,做了一些优化。

getset():这个命令主要有两个参数 getset(key,newValue)。该方法是原子的,对 key 设置 newValue 这个值,并且返回 key 原来的旧值。假设 key 原来是不存在的,那么多次执行这个命令,会出现下边的效果:

  • getset(key, “value1”) 返回 null 此时 key 的值会被设置为 value1
  • getset(key, “value2”) 返回 value1 此时 key 的值会被设置为 value2

使用步骤:

  1. setnx(lockkey, 当前时间+过期超时时间),如果返回 1,则获取锁成功;如果返回 0 则没有获取到锁,转向 2。
  2. get(lockkey) 获取值 oldExpireTime ,并将这个 value 值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向 3。
  3. 计算 newExpireTime = 当前时间+过期超时时间,然后 getset(lockkey, newExpireTime) 会返回当前 lockkey 的值currentExpireTime。
  4. 判断 currentExpireTime 与 oldExpireTime 是否相等,如果相等,说明当前 getset 设置成功,获取到了锁。如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试。
  5. 在获取到锁之后,当前线程可以开始自己的业务处理,当处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行 delete 释放锁;如果大于锁设置的超时时间,则不需要再锁进行处理。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值