分布式系统中,要保证某个资源在一时间段内只能有 一个进程访问,需要使用分布式锁。
1、基于redis的分布式锁
我们首先介绍三个使用到的redis方法SETNX()、GET()、GETSET()。
setnx ( key, value ):SET if Not Exists,该方法是原子的。若 key 不存在,则设置当前 key 成功,返回 1;若 key 已存在,则设置当前 key 失败,返回 0。
getset ( key,newValue ):该方法是原子的,对 key 设置新值 newValue ,返回 key 原来的旧值。若原来无值,则返回null。
使用步骤:
1、setnx(lockkey, 当前时间+过期超时时间),如果返回 1,则获取锁成功;如果返回 0 则没有获取到锁,转向 2
2、get(lockkey) 获取值 oldExpireTime ,并将这个 value 值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向 3
3、计算 newExpireTime = 当前时间+过期超时时间,然后 getset(lockkey, newExpireTime) ,返回当前 lockkey 的(旧)值currentExpireTime。
判断 currentExpireTime 与 oldExpireTime 是否相等,如果相等,则当前 getset 设置成功,获取到了锁;如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试。
4、在获取到锁之后,当前线程可以开始自己的业务处理,处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行 delete 释放锁;如果大于锁设置的超时时间,无需处理。
//redis分布式锁
public final class RedisLockUtil {
private static final int defaultExpire = 60;
private RedisLockUtil() {
//
}
public static boolean lock(String key) {
return lock2(key, defaultExpire);
}
/**
* 加锁
*
* @param key redis key
* @param expire 过期时间,单位秒
* @return true:加锁成功,false,加锁失败
*/
public static boolean lock2(String key, int expire) {
RedisService redisService = SpringUtils.getBean(RedisService.class);
long value = System.currentTimeMillis() + expire;
long status = redisService.setnx(key, String.valueOf(value));
if (status == 1) {
return true;
}
// 获取旧值
long oldExpireTime = Long.parseLong(redisService.get(key, "0"));
if (oldExpireTime < System.currentTimeMillis()) {
// 超时
long newExpireTime = System.currentTimeMillis() + expire;
// 当前旧值
long currentExpireTime = Long.parseLong(redisService.getSet(key, String.valueOf(newExpireTime)));
if (currentExpireTime == oldExpireTime) {
return true;
}
}
return false;
}
public static void unLock2(String key) {
RedisService redisService = SpringUtils.getBean(RedisService.class);
long oldExpireTime = Long.parseLong(redisService.get(key, "0"));
if (oldExpireTime > System.currentTimeMillis()) {
redisService.del(key);
}
}
//——————————————附:使用SETNX()、EXPIRE()做分布式锁——————————————————
// 此种方案不完善。比如在expire()命令执行成功前,发生了宕机的现象,那就会出现死锁的问题
/**
* 加锁
*
* @param key redis key
* @param expire 过期时间,单位秒
* @return true:加锁成功,false,加锁失败
*/
public static boolean lock(String key, int expire) {
RedisService redisService = SpringUtils.getBean(RedisService.class);
long status = redisService.setnx(key, "1");
if (status == 1) {
redisService.expire(key, expire);
return true;
}
return false;
}
public static void unLock(String key) {
RedisService redisService = SpringUtils.getBean(RedisService.class);
redisService.del(key);
}
}
使用锁:
public void drawRedPacket(long userId) {
String key = "draw.redpacket.userid:" + userId;
boolean lock = RedisLockUtil.lock2(key, 50);
if(lock) {
try {
//领取操作
} finally {
//释放锁
RedisLockUtil.unLock2(key);
}
} else {
new RuntimeException("重复领取奖励");
}
}
优点: 性能高
缺点:失效时间设置多长时间为好?如何设置的失效时间太短,方法没等执行完,锁就自动释放了,那么就会产生并发问题。如果设置的时间太长,其他获取锁的线程就可能要平白的多等一段时间。
2、基于zookeeper的分布式锁
1.在 /lock 节点下创建一个有序临时节点 (EPHEMERAL_SEQUENTIAL)。
2.判断创建的节点序号是否最小,如果是最小则获取锁成功。不是则取锁失败,然后 watch 序号比本身小的前一个节点。
3.当取锁失败,设置 watch 后则等待 watch 事件到来后,再次判断是否序号最小。
4.取锁成功则执行代码,最后释放锁(删除该节点)。
public class DistributedLock implements Lock, Watcher {
private ZooKeeper zk;
private String root = "/locks";// 根
private String lockName;// 竞争资源的标志
private String waitNode;// 等待前一个锁
private String myZnode;// 当前锁
private CountDownLatch latch;// 计数器
private int sessionTimeout = 30000;
private List<Exception> exception = new ArrayList<Exception>();
/**
* 创建分布式锁,使用前请确认config配置的zookeeper服务可用
*
* @param config 127.0.0.1:2181
* @param lockName 竞争资源标志,lockName中不能包含单词lock
*/
public DistributedLock(String config, String lockName) {
this.lockName = lockName;
// 创建一个与服务器的连接
try {
zk = new ZooKeeper(config, sessionTimeout, this);
Stat stat = zk.exists(root, false);
if (stat == null) {
// 创建根节点
zk.create(root, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
} catch (IOException e) {
exception.add(e);
} catch (KeeperException e) {
exception.add(e);
} catch (InterruptedException e) {
exception.add(e);
}
}
/**
* zookeeper节点的监视器
*/
public void process(WatchedEvent event) {
if (this.latch != null) {
this.latch.countDown();
}
}
public void lock() {
if (exception.size() > 0) {
throw new LockException(exception.get(0));
}
try {
if (this.tryLock()) {
System.out.println("Thread-" + Thread.currentThread().getName() + " " + myZnode + " get lock true");
return;
} else {
waitForLock(waitNode, sessionTimeout);// 等待锁
}
} catch (KeeperException e) {
throw new LockException(e);
} catch (InterruptedException e) {
throw new LockException(e);
}
}
public boolean tryLock() {
try {
String splitStr = "_lock_";
if (lockName.contains(splitStr))
throw new LockException("lockName can not contains \\u000B");
// 创建临时子节点
myZnode = zk.create(root + "/" + lockName + splitStr, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println(myZnode + " is created ");
// 取出所有子节点
List<String> subNodes = zk.getChildren(root, false);
// 取出所有lockName的锁
List<String> lockObjNodes = new ArrayList<String>();
for (String node : subNodes) {
String _node = node.split(splitStr)[0];
if (_node.equals(lockName)) {
lockObjNodes.add(node);
}
}
Collections.sort(lockObjNodes);
if (myZnode.equals(root + "/" + lockObjNodes.get(0))) {
// 如果是最小的节点,则表示取得锁
return true;
}
// 如果不是最小的节点,找到比自己小1的节点
String subMyZnode = myZnode.substring(myZnode.lastIndexOf("/") + 1);
System.out.println("subMyZnode=" + subMyZnode);
waitNode = lockObjNodes.get(Collections.binarySearch(lockObjNodes, subMyZnode) - 1);
System.out.println("waitNode=" + waitNode);
} catch (KeeperException e) {
throw new LockException(e);
} catch (InterruptedException e) {
throw new LockException(e);
}
return false;
}
public boolean tryLock(long time, TimeUnit unit) {
try {
if (this.tryLock()) {
return true;
}
return waitForLock(waitNode, time);
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
private boolean waitForLock(String lower, long waitTime) throws InterruptedException, KeeperException {
// 判断比自己小一个数的节点是否存在,如果不存在则无需等待锁,同时注册监听
Stat stat = zk.exists(root + "/" + lower, true);
if (stat != null) {
System.out.println("Thread-" + Thread.currentThread().getName() + " waiting for " + root + "/" + lower);
this.latch = new CountDownLatch(1);
this.latch.await(waitTime, TimeUnit.MILLISECONDS);
this.latch = null;
}
return true;
}
public void unlock() {
try {
System.out.println(Thread.currentThread().getName() + " unlock " + myZnode);
zk.delete(myZnode, -1);
myZnode = null;
zk.close();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
public void lockInterruptibly() throws InterruptedException {
this.lock();
}
public Condition newCondition() {
return null;
}
public class LockException extends RuntimeException {
private static final long serialVersionUID = 1L;
public LockException(String e) {
super(e);
}
public LockException(Exception e) {
super(e);
}
}
public static void main(String[] args) {
final DistributedLock client1 = new DistributedLock("127.0.0.1", "lName");
final DistributedLock client2 = new DistributedLock("127.0.0.1", "lName");
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
client1.lock();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
client1.unlock();
}
}, "t1").start();
// 确保t1启动
try {
Thread.sleep(2000);
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
client2.lock();
System.out.println("do something");
client2.unlock();
}
}, "t2").start();
}
}
优点:
有效的解决单点问题,不可重入问题,非阻塞问题以及锁无法释放的问题。
缺点:
需要引入Zookeeper集群,比较重量级
因为需要频繁的创建和删除节点,性能上不如Redis方式
ZK 中创建和删除节点只能通过 Leader 服务器来执行,然后将数据同步到所有的 Follower 机器上。