分布式锁实现
-
使用Redis的SETNX命令实现分布式锁。
方案:Redis SETNX 语法 SETNX key value 当设置成功时会返回1表示该key没有资源占用反之返回为0表示该资源被占
为每个锁设置一个生命时间,防止独占锁而没有释放导致阻塞(比如异常),在程序的finally块内释放锁资源,在释
放锁资源后将链接Redis的对象返回给链接池或者关闭,否则会导致等待阻塞超时抛异常 -
解决死锁问题
原因:假如一个客户端成功获得锁之后还没有来得及设置生命周期程序发送奔溃或者不能释放锁资源
方案:可以通过锁的键对应的时间戳来判断这种情况是否发生了,如果当前的时间已经大于lock的值,说明该锁已失效,可以被重新使用
package com.king.lock;
import redis.clients.jedis.Jedis;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
/**
* Created by Administrator on 2017/9/9.
*/
public class RedisLock {
private final Jedis jedis;
/**锁定资源的key*/
private final String lockName;
private final String lockValue;
/**拥有锁的最长时间/秒*/
private int expireTime = 30;
/**获取不到锁的等待时间/豪秒*/
private int sleepTime = 10;
/**锁中断状态*/
private boolean interruped = true;
/**超时时间*/
private long expireTimeOut = 0;
private boolean lock = false;
public RedisLock(Jedis jedis,String lockName,String lockValue){
this.lockName = lockName;
this.lockValue = lockValue;
this.jedis = jedis;
}
public void unlock() {
try {
long leftTime = System.currentTimeMillis()+calcSeconds(this.expireTime,TimeUnit.SECONDS);
//当前时间小于过期时间,则锁未超时,删除锁定
if (System.currentTimeMillis() < leftTime)
jedis.del(lockName);
}catch (Exception e){
e.printStackTrace();
} finally {
if(null!=jedis){
jedis.close();
}
}
}
public Condition newCondition() {
throw new UnsupportedOperationException("不支持当前的操作");
}
/**
* 时间转换成毫秒
* @param time
* @param unit
* @return
*/
private long calcSeconds (long time, TimeUnit unit){
if (unit == TimeUnit.DAYS)
return time * 24 * 60 * 60 * 1000;
else if (unit == TimeUnit.HOURS)
return time * 60 * 60 * 1000;
else if (unit == TimeUnit.MINUTES)
return time * 60 * 1000;
else
return time * 1000;
}
/**
* 加锁
* 使用方式为:
* lock();
* try{
* executeMethod();
* }finally{
* unlock();
* }
* @param timeout timeout的时间范围内轮询锁/秒
* @param expire 设置锁生命时间/秒
* @return 成功 or 失败
*/
public boolean lock(long timeout,int expire){
this.expireTime = expire;
timeout = System.currentTimeMillis()+calcSeconds(timeout,TimeUnit.SECONDS);
try {
//在timeout的时间范围内不断轮询锁
while (timeout>System.currentTimeMillis()) {
long lock = jedis.setnx(this.lockName, this.lockValue);
//锁不存在的话,设置锁并设置锁过期时间,即加锁
if (lock == 1L) {
jedis.expire(this.lockName, expire);//设置锁过期时间是为了在没有释放
//锁的情况下锁过期后消失,不会造成永久阻塞
return true;
}
System.out.println("出现锁等待");
//短暂休眠,避免可能的活锁
Thread.sleep(10);
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
}
使用
RedisLock redisLock = new RedisLock(jedisPool.getResource(),"ORDER","1");
try{
boolean lock = redisLock.lock(10, 10);
if(lock){
System.out.println("成功获取锁并秒杀到一个商品:"+SecKillImpl.inventory.get(10000001L));
SecKillImpl.inventory.put(10000001L,SecKillImpl.inventory.get(10000001L)-1);
} else {
System.out.println("没有获取锁");
}
} finally {
if(null!=redisLock){
redisLock.unlock();
}
}
Redis第二种锁方案
- Redis将不再设置过期时间,而是将过期时间当成值设置个key,如果锁已经存在则获取锁的到期时间,和当前时间比较,超时的话,则设置新的值
private String get(final String key) {
Object obj = null;
try {
obj = redisTemplate.execute(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
StringRedisSerializer serializer = new StringRedisSerializer();
byte[] data = connection.get(serializer.serialize(key));
connection.close();
if (data == null) {
return null;
}
return serializer.deserialize(data);
}
});
} catch (Exception e) {
logger.error("get redis error, key : {}", key);
}
return obj != null ? obj.toString() : null;
}
private boolean setNX(final String key, final String value) {
Object obj = null;
try {
obj = redisTemplate.execute(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
StringRedisSerializer serializer = new StringRedisSerializer();
Boolean success = connection.setNX(serializer.serialize(key), serializer.serialize(value));
connection.close();
return success;
}
});
} catch (Exception e) {
logger.error("setNX redis error, key : {}", key);
}
return obj != null ? (Boolean) obj : false;
}
private String getSet(final String key, final String value) {
Object obj = null;
try {
obj = redisTemplate.execute(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
StringRedisSerializer serializer = new StringRedisSerializer();
byte[] ret = connection.getSet(serializer.serialize(key), serializer.serialize(value));
connection.close();
return serializer.deserialize(ret);
}
});
} catch (Exception e) {
logger.error("setNX redis error, key : {}", key);
}
return obj != null ? (String) obj : null;
}
/**
* 获得 lock.
* 实现思路: 主要是使用了redis 的setnx命令,缓存了锁.
* reids缓存的key是锁的key,所有的共享, value是锁的到期时间(注意:这里把过期时间放在value了,没有时间上设置其超时时间)
* 执行过程:
* 1.通过setnx尝试设置某个key的值,成功(当前没有这个锁)则返回,成功获得锁
* 2.锁已经存在则获取锁的到期时间,和当前时间比较,超时的话,则设置新的值
*
* @return true if lock is acquired, false acquire timeouted
* @throws InterruptedException in case of thread interruption
*/
public synchronized boolean lock() throws InterruptedException {
long timeout = sleepTime;
timeout = System.currentTimeMillis()+calcSeconds(timeout,TimeUnit.SECONDS);
while (timeout >= System.currentTimeMillis()) {
long expires = System.currentTimeMillis() + expireTime + 1;
String expiresStr = String.valueOf(expires); //锁到期时间
if (this.setNX(lockName, expiresStr)) {
lock = true;
return true;
}
String currentValueStr = this.get(lockName); //redis里的时间
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
//判断是否为空,不为空的情况下,如果被其他线程设置了值,则第二个条件判断是过不去的
String oldValueStr = this.getSet(lockName, expiresStr);
//获取上一个锁到期时间,并设置现在的锁到期时间,
//只有一个线程才能获取上一个线上的设置时间,因为jedis.getSet是同步的
if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
//防止误删(覆盖,因为key是相同的)了他人的锁——这里达不到效果,这里值会被覆盖,但是因为什么相差了很少的时间,所以可以接受
//[分布式的情况下]:如过这个时候,多个线程恰好都到了这里,但是只有一个线程的设置值和当前值相同,他才有权利获取锁
lock = true;
return true;
}
}
/*
随机延迟, 这里使用随机时间可能会好一点,可以防止饥饿进程的出现,即,当同时到达多个进程,
只会有一个进程获得锁,其他的都用同样的频率进行尝试,后面有来了一些进行,也以同样的频率申请锁,这将可能导致前面来的锁得不到满足.
使用随机的等待时间可以一定程度上保证公平性
*/
Thread.sleep(random.nextInt(DEFAULT_ACQUIRY_RESOLUTION_MILLIS));
}
return false;
}
/**
* Acqurired lock release.
*/
public synchronized void unlock(boolean locks) {
if (lock) {
redisTemplate.delete(lockName);
lock = false;
}
}