背景
对于锁大家肯定不会陌生,在单体系统中, Java 提供的 synchronized 关键字和 ReentrantLock 可重入锁基本能满足我们的需求。
但是随着分布式的快速发展,本地的加锁往往不能满足我们的需要。因为分布式与单机情况下最大的不同在于其不是多线程而是多进程。
多线程由于可以共享堆内存,因此可以简单的采取内存作为标记存储位置。而进程之间甚至可能都不在同一台物理机上,因此需要将标记存储在一个所有进程都能看到的地方。(即在单机系统中锁对象可以存在内存中共享,而分布式系统往往是隔离的)
说到底,分布式锁实现就是将锁放在中间件中维护,这样无论多少台机器,锁都是共享的。
分布式锁的特性
可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。
这把锁要是一把可重入锁(避免死锁)
这把锁最好是一把阻塞锁(根据业务需求考虑要不要这条)
这把锁最好是一把公平锁(根据业务需求考虑要不要这条)
有高可用的获取锁和释放锁功能
获取锁和释放锁的性能要好
何谓可重入锁?
简单来说,就是同一个线程可以对同一把锁可以多次获得,当然,释放锁也需要多次释放
贴下代码
何谓阻塞锁?
从侧面来说,非阻塞的锁指的是 没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
简单粗暴的话,实现搞一个 while 循环,直到获取锁成功为止。
何谓公平锁?
表示线程获取锁的顺序是按照加锁的顺序来分配的,及先来先得,先进先出的顺序,或者线程多次获取锁不成功,可以增加权重,提高获取锁的机会。
高可用的释放锁功能:
锁要设置失效时间,因为一旦释放锁操作失败,其他线程无法再获得到锁
实现
通过注解结合AOP来实现分布式锁。
好处:灵活设置需要锁定的字段。
方案一:基于 REDIS 的 SETNX()、EXPIRE() 方法做分布式锁
1、setnx(lockkey, 1) 如果返回 0,则说明占位失败;如果返回 1,则说明占位成功
2、expire() 命令对 lockkey 设置超时时间,为的是避免死锁问题。
3、执行完业务代码后,可以通过 delete 命令删除 key。
方案一有缺陷,如果在第一步 setnx 执行成功后,在 expire() 命令执行成功前,发生了宕机的现象,那么就依然会出现死锁的问题
方案二: 在方案一基础上进行改善,利用redisTemplate提供的setIfAbsent(),即设置值的时候同是设置过期时间
下面的Coding是基于方案二实现的。
1、引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
2、属性配置
spring.redis.host=192.168.68.110
spring.redis.port=6379
spring.redis.password=123456
3、注解
3.1、创建一个 DistributedLock注解,作用于方法上,属性配置如下
/**
* 锁的注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DistributedLock {
/**
* redis 锁key的前缀
*
* @return redis 锁key的前缀
*/
String prefix() default "distributedLock";
/**
* 过期秒数,默认为10秒
*
*/
int expire() default 10;
/**
* 重试时间,默认为30秒
*
*/
int retry() default 10;
/**
* 超时时间单位
*
* @return 秒
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
/**
* Key的分隔符(默认 :)
* @return String
*/
String delimiter() default ":";
/**
* 当不使用@LockKey注解时或者注解失效,是否锁整个方法
* @return
*/
boolean isLockMethod() default false;
}
3.2、创建一个 LockKey注解,作用于方法参数上,即具体要锁住的属性字段,属性配置如下
/**
* 要锁住的属性字段
* 注意:只支持方法形参(基本类型、String或者自定义实体的第一层属性)加注解,用法可参考第6示例
* 例如:test(@LockKey int stuNo);
* test(Student input);
* Student:{
* @LockKey int stuNo;
* }
*
*/
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface LockKey {
}
4、Lock拦截器(AOP)
@Aspect
@Configuration
@Slf4j
public class LockMethodAspect {
@Autowired
public LockMethodAspect(StringRedisTemplate lockRedisTemplate) {
this.lockRedisTemplate = lockRedisTemplate;
}
private final StringRedisTemplate lockRedisTemplate;
@Around("execution(public * *(..)) && @annotation(com.test.DistributedLock)")
public Object interceptor(ProceedingJoinPoint pjp) throws Throwable{
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
DistributedLock lock = method.getAnnotation(DistributedLock.class);
if (StringUtils.isEmpty(lock.prefix())) {
throw new RuntimeException("lock key prefix can't be null...");
}
final String lockKey = RedisLockUtils.getLockKey(pjp);
boolean flag = false;
try {
//key不存在才能设置成功
flag = RedisLockUtils.lock(lockRedisTemplate, lockKey, lock.expire(), lock.retry(), lock.timeUnit());
} catch (Exception e) {
log.error("redis 异常", e);
//TODO 自行处理
}
if (flag) {
try {
return pjp.proceed();
} catch (Exception e) {
log.error("分布式锁的执行异常", e);
//TODO 自行处理
}finally {
RedisLockUtils.unLock(lockRedisTemplate,lockKey);
}
} else {
//按理来说 我们应该抛出一个自定义的 CacheLockException 异常;
throw new RuntimeException("获取分布式锁失败");
}
}
}
5、Lock的工具类
@Slf4j
public class RedisLockUtils {
/**
* 加锁
*
* @param key redis key
* @param expire 过期时间,单位秒
* @param retry 重试时间,单位秒
* @return true:加锁成功,false,加锁失败
*/
public static boolean lock(RedisTemplate redisTemplate, String key, int expire, int retry, TimeUnit timeUnit) {
long currentTimeMillis = System.currentTimeMillis();
long value = currentTimeMillis + expire * 1000;
long end = currentTimeMillis+ retry * 1000;
while ( System.currentTimeMillis() <= end) {
boolean status = false;
//long status = redisService.setnx(key, String.valueOf(value));
try {
status = redisTemplate.opsForValue().setIfAbsent(key, String.valueOf(value), expire, timeUnit);
} catch (Exception e) {
log.error("resdis 异常", e);
break;
}
if (status) {
return true;
}
//重试
if (!status && retry>0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
return false;
}
public static void unLock(RedisTemplate redisTemplate, String key) {
String value = (String) redisTemplate.opsForValue().get(key);
if (StringUtils.isNotBlank(value)) {
try {
redisTemplate.delete(key);
} catch (Exception e) {
log.error("resdis 异常", e);
}
}
}
public static String getLockKey(ProceedingJoinPoint pjp) {
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
DistributedLock lockAnnotation = method.getAnnotation(DistributedLock.class);
Object[] args = pjp.getArgs();
Parameter[] parameters = method.getParameters();
StringBuilder builder = new StringBuilder();
//默认解析方法里面带 LockKey 注解的属性,如果没有尝试着解析实体对象中的
for (int i = 0; i < parameters.length; i++) {
LockKey annotation = parameters[i].getAnnotation(LockKey.class);
if (annotation == null) {
continue;
}
builder.append(lockAnnotation.delimiter()).append(args[i]);
}
if (StringUtils.isEmpty(builder.toString())) {
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
for (int i = 0; i < parameterAnnotations.length; i++) {
Object object = args[i];
Field[] fields = object.getClass().getDeclaredFields();
for (Field field : fields) {
LockKey annotation = field.getAnnotation(LockKey.class);
if (annotation == null) {
continue;
}
field.setAccessible(true);
builder.append(lockAnnotation.delimiter()).append(ReflectionUtils.getField(field, object));
}
}
}
//如果属性和实体对象都没有LockKey注解,则锁住整个方法
if (StringUtils.isEmpty(builder.toString()) && lockAnnotation.isLockMethod()) {
builder.append(lockAnnotation.delimiter()).append(method.getName());
}
return lockAnnotation.prefix() + builder.toString();
}
}
6、用法示例
@RequestMapping(value="/")
@RestController
public class IndexController {
@DistributedLock(prefix = "testLock")
@RequestMapping(value = "/testLock", method = RequestMethod.GET)
public AjaxResponse testLock(String request) {
return new AjaxResponse();
}
@DistributedLock(prefix = "testLock2")
@PostMapping(value = "/testLock2")
public AjaxResponse testLock2(@LockKey int id, Object yy, Object xxxx) {
return new AjaxResponse();
}
@DistributedLock(prefix = "testLock3")
@PostMapping(value = "/testLock3")
public AjaxResponse testLock3(@RequestBody Student input) {
return new AjaxResponse();
}
}
@Setter
@Getter
public class Studentimplements Serializable {
/**
* 学生编号
*/
@LockKey
private String stuNo;
/**
* 学生姓名
*/
private String name;
}
以上是自己学习过程的理解,如有不妥,谢谢指正~
点个赞再走哦,哈哈哈