基于redis实现分布式锁

一、什么是分布式锁?

分布式锁就是控制分布式系统或者不同系统之间共同访问共享资源的一种锁实现,如果不同的系统或同一个系统的不同主机之间共享了某个资源,往往需要互斥来防止彼此干扰保证一致性。

二、分布式锁的特性

  • 互斥性:在任意时刻, 只能有一个客户端持有锁
  • 避免死锁:即使有一个客户端在持有锁期间因崩溃而没有主动释放锁,也能保证其他客户端获取到锁
  • 容错性:只要大部分redis节点正常运行,客户端就可以加锁和解锁
  • 解铃还须系铃人:加锁和解锁必须是同一个客户端

三、代码实现

需要注意的是,笔者下面是基于spring引入redis实现的分布式锁。

  1. 引入POM依赖
 <dependency>
     <groupId>redis.clients</groupId>
     <artifactId>jedis</artifactId>
 </dependency>
  1. 添加redis配置
<bean id="redisHelper" class="com.hshbic.cloud.rcs.common.RedisHelper">
        <property name="host" value="${redis.host}"></property>
        <property name="port" value="${redis.port}"></property>
         <property name="database" value="${redis.database}"></property>
        <property name="password" value="${redis.password}"></property>
        <property name="maxActive" value="${redis.pool.max-active}"></property>
        <property name="maxWait" value="${redis.pool.max-wait}"></property>
        <property name="maxIdle" value="${redis.pool.max-idle}"></property>
        <property name="minIdle" value="${redis.pool.min-idle}"></property>
        <property name="timeout" value="${redis.pool.timeout}"></property>
        <property name="testOnBorrow" value="${redis.pool.testOnBorrow}"></property>
        <property name="testOnReturn" value="${redis.pool.testOnReturn}"></property>
</bean>
redis.host=localhost
redis.port=6370
redis.database=0
redis.password=123456
redis.pool.max-active=20
redis.pool.max-wait=-1
redis.pool.max-idle=8
redis.pool.min-idle=0
redis.pool.timeout=60000
redis.pool.testOnBorrow=false
redis.pool.testOnReturn=true
  1. 添加redis的配置类
public class RedisHelper {

	private String host;
	private int port;
	private Integer database;
	private String password;
	private int maxActive;
	private int maxWait;
	private int maxIdle;
	private int minIdle;
	private int timeout;
	private boolean  testOnBorrow;
	private boolean testOnReturn;
	private static JedisPool pool;

	@PostConstruct
	private void init() {
		JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
		jedisPoolConfig.setMaxTotal(maxActive);
		jedisPoolConfig.setMaxIdle(maxIdle);
		jedisPoolConfig.setMaxWaitMillis(maxWait);
		jedisPoolConfig.setMinIdle(minIdle);
		jedisPoolConfig.setTestOnBorrow(testOnBorrow);// jedis 第一次启动时,会报错
		jedisPoolConfig.setTestOnReturn(testOnReturn);

		pool = new JedisPool(jedisPoolConfig, host, port, timeout, password, database);
	}

	public void close() {
		if (pool != null) {
			pool.destroy();
		}
	}

	private String buildKey(String key) {
		return LOCK_HEARD + key;
	}

	public String getHost() {
		return host;
	}

	public void setHost(String host) {
		this.host = host;
	}

	public int getPort() {
		return port;
	}

	public void setPort(int port) {
		this.port = port;
	}

	public Integer getDatabase() {
		return database;
	}

	public void setDatabase(Integer database) {
		this.database = database;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public int getMaxActive() {
		return maxActive;
	}

	public void setMaxActive(int maxActive) {
		this.maxActive = maxActive;
	}

	public int getMaxWait() {
		return maxWait;
	}

	public void setMaxWait(int maxWait) {
		this.maxWait = maxWait;
	}

	public int getMaxIdle() {
		return maxIdle;
	}

	public void setMaxIdle(int maxIdle) {
		this.maxIdle = maxIdle;
	}

	public int getMinIdle() {
		return minIdle;
	}

	public void setMinIdle(int minIdle) {
		this.minIdle = minIdle;
	}

	public int getTimeout() {
		return timeout;
	}

	public void setTimeout(int timeout) {
		this.timeout = timeout;
	}

	public boolean isTestOnBorrow() {
		return testOnBorrow;
	}

	public void setTestOnBorrow(boolean testOnBorrow) {
		this.testOnBorrow = testOnBorrow;
	}

	public boolean isTestOnReturn() {
		return testOnReturn;
	}

	public void setTestOnReturn(boolean testOnReturn) {
		this.testOnReturn = testOnReturn;
	}
}
  1. 在redis配置类中添加获取锁和释放锁的代码
	private static final String LOCK_SUCCESS = "OK";
	private static final String SET_IF_NOT_EXIST = "NX";
	private static final String SET_WITH_EXPIRE_TIME = "PX";
	private static final String SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
	private static final Long RELEASE_SUCCESS = 1L;
	private static final String LOCK_HEARD = "lock:";
/**
	 * 尝试获取分布式锁
	 * 
	 * @param jedis      Redis客户端
	 * @param lockKey    锁
	 * @param requestId  请求标识
	 * @param expireTime 超期时间
	 * @return 是否获取成功
	 */
	public boolean tryGetDistributedLock(String lockKey, String requestId, int expireTime) {
		Jedis jedis = null;
		Object result = null;
		boolean ret = false;
		try {
			jedis = getConnection();
			result = jedis.set(buildKey(lockKey), requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
		} catch (Exception e) {
			log.error("", e);
		} finally {
			try {
				closeJedis(jedis);
			} catch (Exception e) {
				log.error("", e);
			}
			if (LOCK_SUCCESS.equals(result)) {
				ret = true;
			}
		}
		return ret;
	}

	/**
	 * 释放分布式锁
	 * 
	 * @param jedis     Redis客户端
	 * @param lockKey   锁
	 * @param requestId 请求标识
	 * @return 是否释放成功
	 */
	public boolean releaseDistributedLock(String lockKey, String requestId) {
		Jedis jedis = null;
		Object result = null;
		boolean ret = false;
		try {
			jedis = getConnection();
			result = jedis.eval(SCRIPT, Collections.singletonList(buildKey(lockKey)),
					Collections.singletonList(requestId));
		} catch (Exception e) {
			log.error("", e);
		} finally {
			try {
				closeJedis(jedis);
			} catch (Exception e) {
				log.error("", e);
			}
			if (RELEASE_SUCCESS.equals(result)) {
				ret = true;
			}
		}
		return ret;
	}
	/**
	 * 获取请求Id
	 * 
	 * @return 返回UUID
	 */
	public String getRequestId() {
		return UUID.randomUUID().toString();
	}

可以看到在获取分布式锁方法tryGetDistributedLock中,jedis.set(String key, String value, String nxxx, String expx, int time),这个set()方法一共有五个形参:

  • 第一个为key,我们使用key来当锁,因为key是唯一的。

  • 第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。

  • 第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOTEXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;

  • 第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。

  • 第五个为time,与第四个参数相呼应,代表key的过期时间。
    通过如上,我们可以看出执行set()方法会有两张结果:1、当前key不存在(未加锁),那么就进行加锁并设置有效期,同时value表示加锁的客户端。2、 已有key存在时(已加锁),不做任何操作。
    细心的同学也许会发现上面的代码也同时满足分布式锁特性的几个特点:首先,set()加入了NX参数,可以保证如果有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足了互斥性。其次, 由于对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有释放锁,锁也会因为到了过期时间而自动解锁,不会发生死锁。最后,我们将value设置为requestId,代表客户端的请求标识,那么客户端在解锁的时候可以校验是否是同一个客户端。 由于目前是redis单机,暂时不考虑容错性。
    在释放锁releaseDistributedLock方法中,jedis.eval()执行了一段lua脚本,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。

  1. 代码加锁
 public void saveOrder() {

        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setOrderId("product_01");
        orderInfo.setProductName("apple");
        // 上锁
        String requestId = null;
        try {
            requestId = redisHelper.getRequestId();
            boolean lock = redisHelper.tryGetDistributedLock(orderInfo.getOrderId(), requestId, 10000);
            if (lock) {
                saveOrderForDB(orderInfo);
            }

        } finally {
            try {
                // 持有锁则释放
                redisHelper.releaseDistributedLock(orderInfo.getOrderId(), requestId);
            } catch (Exception e) {
                log.error("Lock release Error:{}", e);
            }
        }
    }

四、总结

本文主要介绍了基于redis如何实现分布式锁。通过上文的学习,再经过自己的实践,相信读者就可以轻松的掌握。如有问题,也积极欢迎和笔者交流。

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值