摘要
当在分布式模型下,数据只有一份,此时需要利用锁的技术控制某一时刻修改数据的进程数。与单机模式下的锁不同,分布式锁不仅仅需要保证不同进程访问对象有锁,还需要保证不同主机的不同系统访问该对象时有锁。所以通常我们会为需要枷锁的对象添加状态,分布式系统中锁的状态通常存储在外部公共存储中,例如redis、zookeeper、文件系统甚至数据库中。
本文将基于redis实现分布式锁。
加锁
加锁其实就是向redis添加key-value。为了避免死锁,需要设置过期时间。
NX 代表只在键不存在时,才对键进行设置操作
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(); } } } }}