Redis分布式锁讲解

前言:在我们做的商城项目中,为了实现在多Tomcat集群环境下定时关单(用户2小时未支付订单则需要取消订单),需要分布式锁。

由于定时关单是周期性的执行某个函数(可以使用Spring Schedule实现),多Tomcat集群环境下,会造成同一时间多个Tomcat同时执行关单任务,造成不必要的资源消耗,所以引入了分布式锁的概念(互斥锁)

(注意:分布式锁应用非常广,还可以应用在秒杀业务,解决缓存穿透,雪崩等问题上,大家必须了解原理并吃透它)

正文

首先由于我们的缓存工具使用的是redis,所以先来了解下redis几个原子命令:

setnx 先判断键是否存在,key存在设置失败,返回0;否则成功返回1

getset 获取旧的值,设置新的值

expire 设置键的有效期

del 删除键

普通流程图

如上图所示:首先使用setnx存入key=锁的名字,value=当前时间+超时时间,如果设置成功,expire给锁设置一个有效期,接着执行业务删除锁;否则设置失败,直接结束

代码演示:

楼上缺点:会出现死锁现象,启动双Tomcat,当一个项目运行到Long setnxResult=RedisShardedPoolUtil.setnx(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK,String.valueOf(System.currentTimeMillis()+lockTimeout));时,突然服务器需要更新项目重启了,此时redis里的CLOSE_ORDER_TASK_LOCK还没expire设置过期时间,这时另外一个Tomcat永远无法获得锁。死锁现象发生

当出现上述问题时,说明此分布式锁需要进一步优化,以下是两种解决方案

(1)设置delLock方法,当强制关闭Tomcat时调用此方法

缺点:当有许多Tomcat时或delLock的东西太多时会消耗太多时间;当我们使用kill掉Tomcat进程时,此方法不会调用。

(2)Redis分布式锁双重防死锁演进(重点!!!!)

优化流程图

如上图所示:之前部分步骤一样,只是当获取锁失败后,使用get获取当前锁的value=lockvalueA,如果当前的时间>lockvalueA,说明setnx时timeout现在已经超时了,此时有权利获取锁;此时为true再使用getset方法重新设置一个当前的value值并返回旧的value=lockvalueB,这时做判断,如果lockvalueB==null(锁已经没有了,可以获取到锁)或者lockvalueA==lockvalueB(最新获取的B与之前得到的A相等,在这个过程中锁没有变化)满足则获取锁成功,执行expire业务逻辑,否则结束。

代码演示:

上述分布式锁是基于setnx操作实现的阻塞式分布式锁,是悲观锁

--------------------------------------------------------------------------------------

下面我们基于秒杀系统来讲解下乐观锁的实现(同样可用于定时关单)

乐观锁:使用数据版本(version)记录机制实现,即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则可以理解为加锁,如果取出的当前version的值与第一次取出来的值不等,就会更新失败,我们可以把这个过程理解为加锁失败,只有加锁成功的那个线程才能进行后面的业务逻辑操作。

有两种方式实现:

(1)仅用数据库来实现乐观锁,我们其实只需要在数据库中添加version字段,并执行一个核心sql语句即可:

update status set version=(version+1) where version=version

(2)用redis实现

我们先来了解下redis事务和相关命令

Redis中的事务(transaction)是一组命令的集合。事务同命令一样都是Redis最小的执行单位,一个事务中的命令要么都执行,要么都不执行。Redis事务的实现需要用到 MULTI 和 EXEC 两个命令,事务开始的时候先向Redis服务器发送 MULTI 命令,然后依次发送需要在本次事务中处理的命令,最后再发送 EXEC 命令表示事务命令结束。

multi,开启Redis的事务,置客户端为事务态

exec,提交事务,执行从multi到此命令前的命令队列,置客户端为非事务态(事务执行成功返回正数,失败返回null)

详细用法请看:http://www.runoob.com/redis/transactions-exec.html

discard,取消事务,置客户端为非事务态

watch,监视键值对,作用时如果事务提交exec时发现监视的监视对发生变化,事务将被取消

redis乐观锁实现分布式锁:使用watch命令会监视给定的key,当exec时候如果监视的key从调用watch后发生过变化,则整个事务会失败。也可以调用watch多次监视多个key。这样就可以对指定的key加乐观锁了。注意watch的key是对整个连接有效的,事务也一样。如果连接断开,监视和事务都会被自动清除。当然了exec,discard,unwatch命令都会清除连接中的所有监视。

代码演示

100个商品秒杀,但有10000个顾客参与

模拟消费者核心(上面OptimisticLockTest内部类)

上述分布式锁是基于watch方法监视商品键值对,事务提交exec时发现监视的键值对发生变化,事务将被取消,任务执行无效,是乐观锁

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20180613G0N5Z400?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券