单机版应用可以用锁(lock、synchronized)解决共享资源操作问题,但在分布式系统下 比如多个进程同时对一个资源进行修改,我们无法控制 ,中间必须加一层中间件来控制共享资源的操作,目前比较流行的分布式锁基于zookeeper、redis、数据库等来实现,本文主要是基于redis实现分布式锁。
说下redis基本命令
SETNX key value
SETNX是key如果存在则返回0,不存在则返回1,如下图所示
那么说白了就是:== redis作为一个中间件来控制分布式系统中多个服务操作 共享资源 ==
单机版可以换成lock、synchronized方式,就如下图
public class LockCase1 extends RedisLock {
public LockCase1(Jedis jedis, String name) {
super(jedis, name);
}
@Override
public void lock() {
while(true){
String result = jedis.set(lockKey, "value", NOT_EXIST);
if(OK.equals(result)){
System.out.println(Thread.currentThread().getId()+"加锁成功!");
break;
}
}
}
@Override
public void unlock() {
jedis.del(lockKey);
}
}
这个案例很简单,直接set一个key,如果不存在则成功。
如果获得锁的线程挂了,那么这个锁就永远不会被释放,其他线程获取不到锁。
public class LockCase2 extends RedisLock {
public LockCase2(Jedis jedis, String name) {
super(jedis, name);
}
@Override
public void lock() {
while(true){
/**
* 这里设置key和设置过期时间需要保持原子性,
* 设置存活时间30秒,解决LockCase1存在的问题.
* @see com.learnRedis.lock.case1.LockCase1
*/
String result = jedis.set(lockKey, "value", NOT_EXIST,SECONDS,30);
if(OK.equals(result)){
System.out.println(Thread.currentThread().getId()+"加锁成功!");
break;
}
}
}
@Override
public void unlock() {
jedis.del(lockKey);
}
}
这个案例在案例1的基础上增加一个过期时间,解决案例1的问题。
public class LockCase3 extends RedisLock {
public LockCase3(Jedis jedis, String name) {
super(jedis, name);
}
@Override
public void lock() {
while(true){
/**
* 设置value为当前线程特有的值
*/
String result = jedis.set(lockKey, lockValue, NOT_EXIST,SECONDS,30);
if(OK.equals(result)){
System.out.println(Thread.currentThread().getId()+"加锁成功!");
break;
}
}
}
@Override
public void unlock() {
/**
* 此处不具备原子性,可以分为三个步骤
* 1.获取锁对应的value值
* 2.检查是否与requestId相等
* 3.如果相等则删除锁(解锁)
*/
String lockValue = jedis.get(lockKey);
if (lockValue.equals(lockValue)){
jedis.del(lockKey);
}
}
在案例2的基础上,添加了唯一id value值,释放锁时判断,key对应的value是否是锁他的那个线程。
解锁过程(查询、比较、删除)不具备原子性
public class LockCase4 extends RedisLock {
public LockCase4(Jedis jedis, String lockKey) {
super(jedis, lockKey);
}
@Override
public void lock() {
while (true) {
String result = jedis.set(lockKey, lockValue, NOT_EXIST, SECONDS, 30);
if (OK.equals(result)) {
System.out.println(Thread.currentThread().getId() + "加锁成功!");
break;
}
}
}
@Override
public void unlock() {
// 使用lua脚本进行原子删除操作
String checkAndDelScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else " +
"return 0 " +
"end";
jedis.eval(checkAndDelScript, 1, lockKey, lockValue);
}
在案例3的基础上,解锁时添加lua脚本(原子删除操作)。
这个案例在很多项目中其实已经是最终版本了,设置一个执行的超时时间;但为了保证 过期时间大于执行时间,增加案例5
/**
* 刷新key的过期时间
*/
private class ExpirationRenewal implements Runnable{
@Override
public void run() {
while (isOpenExpirationRenewal){
System.out.println("执行延迟失效时间中...");
String checkAndExpireScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('expire',KEYS[1],ARGV[2]) " +
"else " +
"return 0 end";
jedis.eval(checkAndExpireScript, 1, lockKey, lockValue, "30");
//休眠10秒
sleepBySencond(10);
}
}
}
在案例4的基础上,增加上述方法,刷新key的过期时间,完整源码在下面链接。
https://gitee.com/yubin0707/daily 代码地址.
第一篇博客,大佬们看完给点个赞