基于Redis实现分布式锁(一)

        扑街前言:之前的分布式锁概述文章中讲到了分布式锁的特性,那么这次就根据这些特性聊一下Redis是如何实现分布式锁的。(认识自己是菜鸟的第九天)


setnx和expire命令:

        谈及如何基于Redis如何实现分布式锁或者说简单的锁,那么就一定要涉及setnx和expire两个命令。(这里提供一个网址,个人觉得很好用:Redis 命令参考 — Redis 命令参考

  • setnx:只有键key 值不存的情况下,将键key 值设置为值value。如果键key 值存在,则setnx命令没有任何动作。命令在设置成功时返回 1 ,设置失败时返回 0。
  • expire:为给定 key 设置生存时间,当 key 过期时(生存时间为 0 ),它会被自动删除。也可以更新生存时间,对带有生存时间的 key 执行 expire 命令,新指定的生存时间会取代旧的生存时间。而这个命令设置的时间单位为“秒”,这里有个相同的命令pexpire,这个命令的效果是一样的,但是这个的时间单位是“毫秒”。设置成功返回 1 。 当 key 不存在或者不能为 key 设置生存时间时,返回 0 。

        根据这两个命令我们可以先用一段伪代码来实现锁的基本流程,如下:

// 判断key值是否已经存入
if (setnx(key, value) == 1) {
    // 存入则设置生效时间
    expire(key, 30);

    try {
        /*
         * 业务逻辑代码
         */
    } finally {
        // 释放锁
        del(key);
    }
}

RedisTemplate:

        上面的代码只是一段基本逻辑,那如何用常规的代码实现呢?这里需要提个一个由Spring封装的RedisTemplate对象,这个对象在我看来是同等于Redis提供的RedissonClient(客户端对象)的,下面代码展示一下RedisTemplate对象连接数据库的配置。

        可以看出RedisTemplate对象是有一个connectionFactory属性来封装连接Redis数据库的,而connectionFactory属性来源是RedisTemplate对象的父类对象RedisAccessor中的成员属性RedisConnectionFactor对象。

        配置和来源说清楚了,那在说一下使用,RedisTemplate.opsForValue.setIfAbsent(key, value),这个方法传入的就是key值和value值,用Redis的setnx命令存储。

        还有RedisTemplate.expire(key, time, TimeUnit),key 需要设置的key  timeout:key的生存时间  timeuint:时间单位(小时,分钟,秒),用Redis的pExpire命令执行,所以这里执行的是毫秒数。

        以及RedisTemplate.delete(key)方法,用Redis的del命令删除。具体代码下面代码段展示。

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
    xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:tx="http://www.springframework.org/schema/tx" xmlns:util="http://www.springframework.org/schema/util"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
      http://www.springframework.org/schema/context
      http://www.springframework.org/schema/context/spring-context-3.0.xsd
      http://www.springframework.org/schema/mvc
      http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
        http://www.springframework.org/schema/aop
    http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
        http://www.springframework.org/schema/util 
      http://www.springframework.org/schema/util/spring-util-3.0.xsd">

<!--[redis-JedisPoolConfig配置](http://blog.csdn.net/liang_love_java/article/details/50510753)-->
<!--    jedis-2.7.2.jar 依赖jar包 commons-pool2-2.3.jar 
        jedis基于 commons-pool2-2.3.jar 自己实现了一个资源池。
        配置参数 详见 http://blog.csdn.net/liang_love_java/article/details/50510753
-->
    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig"> 
        <property name="maxIdle" value="1" /> 
        <property name="maxTotal" value="5" /> 
        <property name="blockWhenExhausted" value="true" /> 
        <property name="maxWaitMillis" value="30000" /> 
        <property name="testOnBorrow" value="true" />  
    </bean> 

    <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"> 
        <property name="hostName" value="10.1.8.200" /> 
        <property name="port" value="6379"/> 
        <property name="poolConfig" ref="jedisPoolConfig" /> 
        <property name="usePool" value="true"/> 
    </bean> 

    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">   
        <property name="connectionFactory"   ref="jedisConnectionFactory" />   
        <property name="keySerializer">   
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />   
        </property>      
        <property name="valueSerializer">   
            <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer" />   
        </property>   
        <property name="hashKeySerializer">     
           <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>     
        </property>   
        <property name="hashValueSerializer">   
           <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>     
        </property> 
     </bean> 

</beans>
/*
 * (non-Javadoc)
 * @see org.springframework.data.redis.core.ValueOperations#setIfAbsent(java.lang.Object, java.lang.Object)
 */
@Override
public Boolean setIfAbsent(K key, V value) {
    // 转换字节
	byte[] rawKey = rawKey(key);
	byte[] rawValue = rawValue(value);

    // 调用命令执行
	return execute(connection -> connection.setNX(rawKey, rawValue), true);
}

/*
 * (non-Javadoc)
 * @see org.springframework.data.redis.core.RedisOperations#delete(java.lang.Object)
 */
@Override
public Boolean delete(K key) {
    // 转换字节
	byte[] rawKey = rawKey(key);

    // 调用命令执行,返回被删除的数量
	Long result = execute(connection -> connection.del(rawKey), true);

    // 返回是否成功
	return result != null && result.intValue() == 1;
}

/*
 * (non-Javadoc)
 * @see org.springframework.data.redis.core.RedisOperations#expire(java.lang.Object, long, java.util.concurrent.TimeUnit)
 */
@Override
public Boolean expire(K key, final long timeout, final TimeUnit unit) {
    // 转换字节
	byte[] rawKey = rawKey(key);
    // 转换时间
	long rawTimeout = TimeoutUtils.toMillis(timeout, unit);

    // 调用命令执行
	return execute(connection -> {
		try {
			return connection.pExpire(rawKey, rawTimeout);
		} catch (Exception e) {
			// Driver may not support pExpire or we may be running on Redis 2.4
			return connection.expire(rawKey, TimeoutUtils.toSeconds(timeout, unit));
		}
	}, true);
}

        以上代码其实就能实现开始时的伪代码内容,但是setnx和expire的命令是非原子性的操作,这样做会很容易出现死锁的问题,那么目前有两种方案解决:Redis2.6版本之前有lua脚本、2.6版本之后可以用set命令,这里的set命令可以传入多个参数来设置,同时RedisTemplate对象将setIfAbsent方法进行了重载,可以用来实现set命令来设置生命时间,如下展示:

/*
 * (non-Javadoc)
 * @see org.springframework.data.redis.core.ValueOperations#setIfAbsent(java.lang.Object, java.lang.Object, long, java.util.concurrent.TimeUnit)
 */
@Override
public Boolean setIfAbsent(K key, V value, long timeout, TimeUnit unit) {

	byte[] rawKey = rawKey(key);
	byte[] rawValue = rawValue(value);

	Expiration expiration = Expiration.from(timeout, unit);
	return execute(connection -> connection.set(rawKey, rawValue, expiration, SetOption.ifAbsent()), true);
}

错误解锁:

        上述其实就已经实现了一个基本的锁,但是锁不光只有超时、释放等,现在说一下错误解锁的问题,错误解锁就是当业务执行时间过长,Redis设置的过期时间到了,这个锁由Redis自身解开了,然后由第二个线程拿到业务,进行加锁,然后在第二个线程业务执行时,第一个线程业务执行结束,解除了第二个线程的锁。上篇文章有说到锁的特性中有:加锁解锁必然是同一个线程,这面的这种业务就明显做不到。

        解决方案:给锁加一个人为定义的唯一标识来标明当前线程的请求线程唯一ID,用key对应的value值进行存储,当解锁时,判断如果不是当前线程,那么就不解锁,反之解锁。

        注意:一定要保证原子性,这个设置不能放在客户端代码中,可以用lua脚本来实现。Redis中也提供lua脚本的执行命令call命令。(这篇文章就不讲这个了,下次再说)

        还是刚刚说的场景,线程拿到锁、加锁、加过期时间后,执行业务的过程中,锁的过期时间到了,也就是业务执行时间大于锁的过期时间,这样就出现了问题。

        解决方案:为拿到锁的线程,创建一个守护线程,守护线程定时/延迟,判断拿到锁的线程是否还持有锁,是则为锁续期。

        这里提供一个思路:创建线程池,创建一个队列(队列中存的就是线程的唯一ID),添加自动任务线程,定时扫描队列,如果队列中存在值则续期,反之跳过。当线程拿到锁的时候,增加队列,让自动任务去扫描续期即可。缺点就是:服务压力过大,资源浪费。


如何可重入:

  1. 基于本地实现;
  2. 基于Redis的hash类型来实现。

阻塞/非阻塞:

        如何做一个阻塞锁呢?还是两个方案

  1. 基于客户端轮调,也就是说发现已经有线程拿到了锁,然后就隔一段时间就访问一次,直到拿到锁或者到了一定时间就退出报错。(缺点:浪费资源,服务器压力过大)
  2. 基于Redis的发布与订阅:简单来说就是A线程先拿到锁,B线程获取不到的时候,订阅锁释放消息,当A线程解锁时,就会将锁释放的消息放在Redis的消息队列中,然后通知B线程过来获取锁。


 

        当实现上诉的所有内容时,一个完整的分布式锁就创建出来了,这个逻辑不光适用于Redis的分布式锁,也适用于其他的中间件来实现分布式锁的原理。下篇文章再说下Redis本身是如何实现分布式锁的,跟一下源码,分析一下。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值