当在分布式模型下,数据只有一份,此时需要利用锁的技术控制某一时刻修改数据的进程数。与单机模式下的锁不同,分布式锁不仅仅需要保证不同进程访问对象有锁,还需要保证不同主机的不同系统访问该对象时有锁。所以通常我们会为需要枷锁的对象添加状态,分布式系统中锁的状态通常存储在外部公共存储中,例如redis、zookeeper、文件系统甚至数据库中。
本文将基于redis实现分布式锁。
加锁其实就是向redis添加key-value。为了避免死锁,需要设置过期时间。
SET lock_key lock_value NX PX 5000
如果上面的命令执行成功,则证明客户端获取到了锁。
解锁其实就是删除redis中添加的key。但也不能乱删,不能客户端1的请求将客户端2的锁给删除掉。需要根据lock_value过滤。需要注意的是为了保证redis操作的原子性,需要通过lua命令删除。
static {
StringBuilder sb = new StringBuilder();
sb.append("if redis.call('get', KEYS[1]) == ARGV[1] then " );
sb.append(" return redis.call('expire',KEYS[1],ARGV[2]) ");
sb.append(" else ");
sb.append("return 0 ");
sb.append(" end ");
lua_expire=sb.toString();
}
jedis.eval(lua_del, 1, lockKey, lockValue);
为了避免死锁设置了过期时间,同时又要保证过期时间不能低于代码执行时间。所以单独添加一个线程刷新过期时间。同时为了保证redis操作原子性,通过lua脚本执行。
/**
* 开启定时刷新
*/
protected void scheduleExpirationRenewal(){
Thread renewalThread = new Thread(new ExpirationRenewal());
renewalThread.start();
}
private class ExpirationRenewal implements Runnable{
@Override
public void run() {
while (isOpenExpirationRenewal){
try{
System.out.println("[key="+lockKey+"]延迟失效时间");
jedis.eval(lua_expire,1, lockKey, lockValue, String.valueOf(EXPIRE_TIME));
//休眠10秒
sleepBySecond(EXPIRE_TIME-1);
}catch (Exception ex){
ex.printStackTrace();
}
}
}
}
RedisLock lock = new RedisLock("testRedisLock");
lock.lock();
//模拟业务执行15秒
System.out.println("执行方法:"+id);
lock.sleepBySecond(15);
lock.unlock();
public class RedisLock implements Lock {
private static final long EXPIRE_TIME=100;
private static final String NOT_EXIST="NX";
private static final String SECOND="EX";
private static final String OK="OK";
protected volatile boolean isOpenExpirationRenewal = true;
private Jedis jedis=null;
private String lockKey="";
private String lockValue="";
private static String lua_del= "";
private static String lua_expire= "";
static {
StringBuilder sb = new StringBuilder();
sb.append("if redis.call('get', KEYS[1]) == ARGV[1] then ");
sb.append(" return redis.call('del', KEYS[1]) ");
sb.append(" else ");
sb.append("return 0 ");
sb.append(" end ");
lua_del=sb.toString();
}
static {
StringBuilder sb = new StringBuilder();
sb.append("if redis.call('get', KEYS[1]) == ARGV[1] then " );
sb.append(" return redis.call('expire',KEYS[1],ARGV[2]) ");
sb.append(" else ");
sb.append("return 0 ");
sb.append(" end ");
lua_expire=sb.toString();
}
/**
* 获取redis连接,随机生成value
* @param lockKey
*/
public RedisLock(String lockKey){
this.lockKey=lockKey;
this.lockValue= UUID.randomUUID().toString()+Thread.currentThread().getId();
JedisPool pool=SpringContextUtil.getBean(JedisPool.class);
this.jedis= pool.getResource();
}
/**
* 加锁
*/
@Override
public void lock() {
while(true){
String result = jedis.set(lockKey, lockValue, NOT_EXIST,SECOND,EXPIRE_TIME);
if(OK.equals(result)){
System.out.println("[key="+lockKey+"]已加锁");
//添加单独线程刷新过期时间
isOpenExpirationRenewal = true;
scheduleExpirationRenewal();
break;
}
}
}
/**
* 解锁
*/
@Override
public void unlock() {
jedis.eval(lua_del, 1, lockKey, lockValue);
System.out.println("[key="+lockKey+"]已解锁");
isOpenExpirationRenewal = false;
}
@Override
public void lockInterruptibly(){}
@Override
public Condition newCondition() {
return null;
}
@Override
public boolean tryLock() {
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit){
return false;
}
/**
* 线程休眠
* @param second
*/
public void sleepBySecond(long second){
try {
Thread.sleep(second*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 开启定时刷新
*/
protected void scheduleExpirationRenewal(){
Thread renewalThread = new Thread(new ExpirationRenewal());
renewalThread.start();
}
private class ExpirationRenewal implements Runnable{
@Override
public void run() {
while (isOpenExpirationRenewal){
try{
System.out.println("[key="+lockKey+"]延迟失效时间");
jedis.eval(lua_expire,1, lockKey, lockValue, String.valueOf(EXPIRE_TIME));
//休眠10秒
sleepBySecond(EXPIRE_TIME-1);
}catch (Exception ex){
ex.printStackTrace();
}
}
}
}
}