Redis分布式锁机制
基于redis实现的Java分布式锁主要依赖redis的SETNX()命令和DEL()命令,SETNX相当于上锁(lock),DEL相当于释放锁(unlock)。我们只要实现Lock接口重写lock()和unlock()即可。但是这还不够,安全可靠的分布式锁应该满足满足下面三个条件:
什么情况下回不满足上面三个条件呢。多个线程(客户端)同时竞争锁可能会导致多个客户端同时拥有锁。比如,
(1)线程1在master节点拿到了锁(存入key)
(2)master节点在把线程1创建的key写入slave之前宕机了,此时集群中的节点已经没有锁(key)了,包括master节点的slaver节点
(3)slaver节点升级为master节点
(4)线程2向新的master节点发起锁(存入key)请求,很明显,能请求成功。
可见,线程1和线程2同时获得了锁。如果在更高并发的情况,可能会有更多线程(客户端)获取锁
拥有锁的线程(客户端)长时间的执行或者因为某种原因造成阻塞,就会导致锁无法释放(unlock没有调用),其它线程就不能获取锁而而产生无限期死锁的情况。其它线程在执行lock失败后即使粗暴的执行unlock删除key之后也不能正常释放锁,因为锁就只能由获得锁的线程释放,锁不能正常释放其它线程仍然获取不到锁。
设置锁的有效时间(redis的expire命令),不管是什么原因导致的死锁,有效时间过后,锁将会被自动释放
只要有Redis节点正常工作,客户端应该都能获取和释放锁,我们必须用相同的key不断循环向Master节点请求锁,当请求时间超过设定的超时时间则放弃请求锁,这个可以防止一个客户端在某个宕掉的master节点上阻塞过长时间,如果一个master节点不可用了,应该尽快尝试下一个master节点。释放锁比较简单,因为只需要在所有节点都释放锁就行,不管之前有没有在该节点获取锁成功
【RedLock算法流程】:
【Resis分布式锁sping配置依赖】:
4.0.0
com.caox
spring-demo
1.0-SNAPSHOT
war
spring-demo Maven Webapp
http://www.example.com
UTF-8
1.7
1.7
UTF-8
4.1.1.RELEASE
3.2.6
1.7.7
1.2.17
2.5.0
junit
junit
4.11
test
org.springframework
spring-test
${spring.version}
test
org.springframework
spring-webmvc
${spring.version}
org.springframework
spring-core
${spring.version}
org.springframework
spring-beans
${spring.version}
org.springframework
spring-context
${spring.version}
org.springframework
spring-context-support
${spring.version}
org.springframework
spring-aop
${spring.version}
org.springframework
spring-aspects
4.0.9.RELEASE
org.springframework
spring-tx
${spring.version}
org.springframework
spring-web
${spring.version}
org.springframework
spring-jdbc
${spring.version}
org.aspectj
aspectjrt
1.7.4
org.aspectj
aspectjweaver
1.7.4
cglib
cglib
3.1
mysql
mysql-connector-java
5.1.34
log4j
log4j
${log4j.version}
org.slf4j
slf4j-api
${slf4j.version}
org.slf4j
slf4j-log4j12
${slf4j.version}
javax.servlet
javax.servlet-api
3.0.1
provided
org.apache.commons
commons-dbcp2
2.0
org.springframework.data
spring-data-redis
1.4.2.RELEASE
redis.clients
jedis
2.6.2
org.apache.commons
commons-pool2
2.4.2
org.projectlombok
lombok
1.14.4
spring-demo
org.apache.maven.plugins
maven-compiler-plugin
1.7
src/main/java
*.properties
*.xml
true
src/main/java/resources
*.properties
*.xml
true
【RedisLock Java 实现】:
package com.caox.redis;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* @author : nazi
* @version : 1.0
* @date : 2018/12/29 14:22
*/
public class RedisLock implements Lock{
protected StringRedisTemplate redisStringTemplate;
// 存储到redis中的锁标志
private static final String LOCKED = "LOCKED";
// 请求锁的超时时间(ms)
private static final long TIME_OUT = 30000;
// 锁的有效时间(s)
public static final int EXPIRE = 60;
// 锁标志对应的key;
private String key;
// state flag
private volatile boolean isLocked = false;
public RedisLock(String key) {
this.key = key;
@SuppressWarnings("resource")
ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath*:application-context.xml");
redisStringTemplate = (StringRedisTemplate)ctx.getBean("redisStringTemplate");
}
@Override
public void lock() {
//系统当前时间,毫秒
long nowTime = System.nanoTime();
//请求锁超时时间,毫秒
long timeout = TIME_OUT*1000000;
final Random r = new Random();
try {
//不断循环向Master节点请求锁,当请求时间(System.nanoTime() - nano)超过设定的超时时间则放弃请求锁
//这个可以防止一个客户端在某个宕掉的master节点上阻塞过长时间
//如果一个master节点不可用了,应该尽快尝试下一个master节点
while ((System.nanoTime() - nowTime) < timeout) {
//将锁作为key存储到redis缓存中,存储成功则获得锁
if (redisStringTemplate.getConnectionFactory().getConnection().setNX(key.getBytes(),
LOCKED.getBytes())) {
//设置锁的有效期,也是锁的自动释放时间,也是一个客户端在其他客户端能抢占锁之前可以执行任务的时间
//可以防止因异常情况无法释放锁而造成死锁情况的发生
redisStringTemplate.expire(key, EXPIRE, TimeUnit.SECONDS);
isLocked = true;
//上锁成功结束请求
break;
}
//获取锁失败时,应该在随机延时后进行重试,避免不同客户端同时重试导致谁都无法拿到锁的情况出现
//睡眠3毫秒后继续请求锁
Thread.sleep(3, r.nextInt(500));
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void unlock() {
//释放锁
//不管请求锁是否成功,只要已经上锁,客户端都会进行释放锁的操作
if (isLocked) {
redisStringTemplate.delete(key);
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
// TODO Auto-generated method stub
}
@Override
public boolean tryLock() {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
// TODO Auto-generated method stub
return false;
}
@Override
public Condition newCondition() {
// TODO Auto-generated method stub
return null;
}
}
【参考文献】: 分布式缓存技术redis系列(五)——redis实战(redis与spring整合,分布式锁实现)
【springboot + redis分布式 + aop】:
package com.caox.aop;
import com.caox.annotions.RedisSync;
import com.caox.service.IRedisService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.Objects;
/**
* @author : nazi
* @version : 1.0
* @date : 2019/1/3 10:53
*/
@Slf4j
@Aspect
@Component
public class RedisSyncAop {
private static final Logger logger = LoggerFactory.getLogger(RedisSyncAop.class);
@Resource
private IRedisService iRedisService;
@Pointcut("@annotation(com.caox.annotions.RedisSync)")
private void anyMethod(){
}
@Around("anyMethod()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object result = null;
//获得锁
Method method = ((MethodSignature)pjp.getSignature()).getMethod();
String key = method.toString();
RedisSync redisSync = method.getAnnotation(RedisSync.class);
long waitTime = redisSync.waitTime();
long currTime = System.currentTimeMillis();
Boolean state = iRedisService.setNx(key, currTime);
long saveTime = 0L;
while (!state) {
// 之前存在key 并发开始
Long tempSaveTime = iRedisService.get(key, Long.class);
// 若锁被释放
if (tempSaveTime == null) {
// 重新加锁
state = iRedisService.setNx(key, currTime);
continue;
}
// 锁被重新获取
if (!tempSaveTime.equals(saveTime)) {
currTime = System.currentTimeMillis();
saveTime = tempSaveTime;
}
// 判断是否超时
if (saveTime + redisSync.timeout() < currTime) {
// 超时,直接获得锁 获取上一个锁的时间value
Object tempTime = iRedisService.getSet(key, currTime);
if(tempTime == null){
state = iRedisService.setNx(key, currTime);
continue;
}
// 判断锁是否被释放 或 未被抢先获取 saveTime = tempSaveTime; tempTime(获取上一个锁时间value)
if (Objects.equals(saveTime, tempTime)) {
logger.warn("方法:{},执行超时,已被强制解锁!", key);
break;
}
}
// 等待
if(waitTime > 0) {
try {
Thread.sleep(waitTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
state = iRedisService.setNx(key, currTime);
}
// 执行方法
result = pjp.proceed();
Long currSaveTime = iRedisService.get(key, Long.class);
// 判断锁未被判定为超时
if (currSaveTime != null && Objects.equals(currSaveTime, currTime)) {
// 释放锁
iRedisService.del(key);
}
return result;
}
}
【参考文献】:SpringBoot+Redis 从HelloWorld到分布式锁