一、 分布式锁的实现
关于锁,其实我们或多或少都有接触过一些,比如synchronized、 Lock这些,这类锁的目的很简单,在多线程环境下,对共享资源的访问造成的线程安全问题,通过锁的机制来实现资源访问互斥。那么什么是分布式锁呢?或者为什么我们需要通过Redis来构建分布式锁,其实最根本原因就是Score(范围),因为在分布式架构中,所有的应用都是进程隔离的,在多进程访问共享资源的时候我们需要满足互斥性,就需要设定一个所有进程都能看得到的范围,而这个范围就是Redis本身。所以我们才需要把锁构建到Redis中。
Redis里面提供了一些比较具有能够实现锁特性的命令,比如SETEX(在键不存在的情况下为键设置值),那么我们可以基于这个命令来去实现一些简单的锁的操作
二、 分布式锁的实战
分布式锁的实现会采用两种方式来实现,一种自己手动来实现,一种是基于Redission的客户端来实现
基于jedis手动实现分布式锁
代码如下:
/**
* @Project: redis
* @description: redis的分布式锁的手动实现
* @author: sunkang
* @create: 2019-01-12 16:54
* @ModificationHistory who when What
**/
public class DistributedLock {
/**
* 获取锁
* @param lockName 锁的名称
* @param accquireTimeout 获取锁的超时时间
* @param lockTimeout 锁本身过期时间
* @return
*/
public String acquireLock(String lockName ,long accquireTimeout,long lockTimeout){
String identify = UUID.randomUUID().toString();
String lockKey = "lock:"+lockName;
//获取redis客户端
long endTime = System.currentTimeMillis()+ accquireTimeout;
Jedis redis = JedisPoolUtils.getRedis();
int expireTime = (int) (lockTimeout/1000);
try{
while (System.currentTimeMillis() 0){
return true;
}
return false;
}
}
- 测试代码
/**
* @Project: redis
* @description: 分布式测试
* 开启10个线程,模拟十个客户端请求锁
* @author: sunkang
* @create: 2019-01-12 17:45
* @ModificationHistory who when What
**/
public class LockTest extends Thread{
@Override
public void run() {
while(true){
DistributedLock distributedLock=new DistributedLock();
String rs=distributedLock.acquireLock("updateOrder",
2000,5000);
if(rs!=null){
System.out.println(Thread.currentThread().getName()+"-> 成功获得锁:"+rs);
try {
Thread.sleep(1000);
//释放锁
distributedLock.releaseLockWithLua("updateOrder",rs);
} catch (InterruptedException e) {
e.printStackTrace();
}
break;
}
}
}
public static void main(String[] args) {
LockTest lockTest=new LockTest();
for(int i=0;i<10;i++){
new Thread(lockTest,"tName:"+i).start();
}
}
}
- 输出结果 : 基本是锁释放了之后,其他的线程才得到了锁
tName:7-> 成功获得锁:38c683cb-1797-4abb-aa57-200e770f7445
updateOrder开始释放锁:38c683cb-1797-4abb-aa57-200e770f7445
tName:6-> 成功获得锁:873dfe4e-00ee-4d2b-9bc5-bb16e83f8879
updateOrder开始释放锁:873dfe4e-00ee-4d2b-9bc5-bb16e83f8879
tName:8-> 成功获得锁:e6c48dea-87d9-4fd5-ab04-09ed2f725307
updateOrder开始释放锁:e6c48dea-87d9-4fd5-ab04-09ed2f725307
tName:0-> 成功获得锁:e0d764a5-30b5-4b6b-8490-73ffd78727e0
...
总结:
acquireLock的方法主要是竞争锁的时间内,不断轮训去查看lockKey的是否存在,不存在则设置锁,返回锁,存在会一直尝试获取锁,直到获取锁的超时时间结束,然后释放redis连接在释放锁的方式,有两种方式,一种是给锁本身设置超时时间,时间到了,锁本身释放了,一种是自动方式来删除锁,删除锁为了锁在释放过程中的原子性,一个使用了lua脚本,一个使用redis的watch机制和事务删除的方式
基于redisson实现分布式锁
maven依赖:
org.redisson
redisson
2.15.0
redisson的分布式代码如下
/**
* @Project: redis
* @description: 基于redission的api实现分布式锁
* @author: sunkang
* @create: 2019-01-12 18:08
* @ModificationHistory who when What
**/
public class RedissionLock {
public static void main(String[] args) {
Config config = new Config();
//使用单个节点连接,并且设置连接地址
config.useSingleServer().setAddress("redis://192.168.44.129:6379");
RedissonClient redissonClient = Redisson.create(config);
RLock lock = redissonClient.getLock("updateOrder");
try {
//尝试获取锁100秒,锁释放时间为10秒
lock.tryLock(100,10,TimeUnit.SECONDS);
System.out.println("得到锁");
Thread.sleep(100);
//释放锁
lock.unlock();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
基于redission 的实现分布式锁的方式,获取锁的关键代码在org.redisson.RedissonLock#tryLockInnerAsync这个方法上:
RFuture tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"return redis.call('pttl', KEYS[1]);",
Collections.
通过lua脚本来实现加锁的操作
- 判断lock键是否存在,不存在直接调用hset存储当前线程信息并且设置过期时间,返回nil,告诉客户端直接获取到锁。
- 判断lock键是否存在,存在则将重入次数加1,并重新设置过期时间,返回nil,告诉客户端直接获取到锁。
- 被其它线程已经锁定,返回锁有效期的剩余时间,告诉客户端需要等待。
释放锁的关键代码在方法: org.redisson.RedissonLock#unlockInnerAsync
protected RFuture unlockInnerAsync(long threadId) {
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end;" +
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
"else " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; "+
"end; " +
"return nil;",
Arrays.
- 如果lock键不存在,发消息说锁已经可用,发送一个消息
- 如果锁不是被当前线程锁定,则返回nil
- 由于支持可重入,在解锁时将重入次数需要减1
- 如果计算后的重入次数>0,则重新设置过期时间
- 如果计算后的重入次数<=0,则发消息说锁已经可用