在分布式应用中进行逻辑处理时常会遇到并发问题,例如:执行订单修改状态,需要查询订单并更新订单状态,这是非原子性操作,存在并发问题。防止一个用户在同一之间内创建多个订单;以上的问题需要通过分布式锁来解决。
下面来介绍一下redis分布式锁的实现和应用。
使用分布式锁的流程如下所示:
下面主要基于单节点的redis服务进行分析:
通常使用setnx
(set if not exit)指令为客户端上锁。
127.0.0.1:6379> set user timestamp nx ex 30 // 步骤1
OK
value
设置一个唯一的id或者是时间戳,尽量使value值保持唯一。nx
表示 set操作的key值是唯一的,如果redis中存在key,那么再次set的时候会返回nil
ex 30
设置key的超时时间为30s。避免key值长期有效而导致客户端竞争锁导致死锁。执行成功后执行del key value
指令来释放锁。
127.0.0.1:6379> del user timestamp // 步骤2
(integer) 1
在java程序中,通常使用redis lua脚本来执行释放锁。
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
java代码实现分布式锁代码:
package com.hpu.lzl.cache.jedis;
import redis.clients.jedis.Jedis;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
*
* @author:awo
* @time:2019/3/25 下午2:59
* @Description: info
**/
public class RedisDistributeTool {
private Jedis jedis;
// key的过期时间:s
private Long expireTime = 5L;
// 获取锁的超时时间:ms
private Long timeOut = 15L * 1000;
private boolean lock = false;
private String OK = "ok";
// 分布式锁的key值
private String lockKey;
// 分布式锁的value值
private String lockValue;
public static final String UNLOCK_LUA;
private JedisPoolUtil jedisPoolUtil;
static {
StringBuilder sb = new StringBuilder();
sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
sb.append("then ");
sb.append(" return redis.call(\"del\",KEYS[1]) ");
sb.append("else ");
sb.append(" return 0 ");
sb.append("end ");
UNLOCK_LUA = sb.toString();
}
public RedisDistributeTool(String keyLock){
this.lockKey = keyLock;
jedisPoolUtil = JedisPoolUtil.getInstance();
}
public RedisDistributeTool(Long expireTime,Long timeOut,String keyLock){
this.expireTime = expireTime;
this.timeOut = timeOut;
this.lockKey = keyLock;
jedisPoolUtil = JedisPoolUtil.getInstance();
}
/**
* 使用setnx,保证set数据设置expire时间的原子性操作
* 阻塞式,设置获取锁的超时时间
* @return
*/
public boolean tryLock(){
Long currentTime = System.currentTimeMillis();
lockValue = UUID.randomUUID().toString();
while (System.currentTimeMillis() - currentTime < timeOut){
jedis = jedisPoolUtil.getJedis();
try {
if (OK.equalsIgnoreCase(jedis.set(lockKey,lockValue,"nx","ex",expireTime))){
lock = true;
return lock;
}
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}catch (Exception e){
e.printStackTrace();
}finally {
jedisPoolUtil.close();
}
}
System.out.println(Thread.currentThread().getName() + " 获取锁超时");
return false;
}
/**
* 只尝试获取一次
* @return
*/
public boolean tryGetLock(){
try {
lockValue = UUID.randomUUID().toString();
jedis = jedisPoolUtil.getJedis();
if (OK.equalsIgnoreCase(jedis.set(lockKey,lockValue,"nx","ex",expireTime))){
lock = true;
return lock;
}
} catch (Exception e) {
e.printStackTrace();
}finally {
jedisPoolUtil.close();
}
return false;
}
/**
* 使用lua脚本删除key:
* 1. 校验value值的一致性
* 2. 如果value一致,则删除key
*/
public boolean unLock(){
try {
if (lock){
List keys = new ArrayList<>();
keys.add(lockKey);
List values = new ArrayList<>();
values.add(lockValue);
jedis = jedisPoolUtil.getJedis();
Long eval = (Long) jedis.eval(UNLOCK_LUA, keys, values);
// eval == 0 表示锁没有释放
lock = eval == 0;
return eval != 0;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
jedisPoolUtil.close();
}
return false;
}
}
完整代码见github地址:
https://github.com/hpulzl/learning_demo/blob/master/src/main/java/com/hpu/lzl/cache/jedis/RedisDistributeTool.java
redlock锁是redis分布式锁实现较为经典的思路。主要解决了在redis集群环境下获取锁的方式。redisson的RedissonRedLock对象实现了redLock介绍的加锁算法。
这里介绍一下RedissonRedLock的使用
org.redisson
redisson
3.10.5
在redis集群的环境下,将多个RLock来关联一个红锁,每个RLock对象实例来自于不同的Redisson实例。
RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3
// 红锁在大部分节点上加锁成功就算成功。
lock.lock();
...
lock.unlock();
提供tryLock(long waitTime, long leaseTime, TimeUnit unit)
方法,可以设置加锁的失效时间。
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 给lock1,lock2,lock3加锁,如果没有手动解开的话,10秒钟后将会自动解开
lock.lock(10, TimeUnit.SECONDS);
// 为加锁等待30秒时间,并在加锁成功10秒钟后自动解开
boolean res = lock.tryLock(30, 10, TimeUnit.SECONDS);
...
lock.unlock();
具体介绍可以参考一下资料
relock算法描述: http://redis.cn/topics/distlock.html
redissonRedLock使用:https://github.com/redisson/redisson/wiki/8.-分布式锁和同步器