Redission分布式锁 YYDS

前言

随着技术快速发展,数据规模增大,分布式系统越来越普及,一个应用往往会部署在多台机器上(多节点),在有些场景中,为了保证数据不重复,要求在同一时刻,同一任务只在一个节点上运行,即保证某一方法同一时刻只能被一个线程执行。在单机环境中,应用是在同一进程下的,只需要保证单进程多线程环境中的线程安全性,通过 JAVA 提供的 volatile、ReentrantLock、synchronized 以及 concurrent 并发包下一些线程安全的类等就可以做到。而在多机部署环境中,不同机器不同进程,就需要在多进程下保证线程的安全性了。因此,分布式锁应运而生。

常见分布式锁方案对比

分类方案实现原理优点缺点
基于数据库基于mysql 表唯一索引1.表增加唯一索引 2.加锁:执行insert语句,若报错,则表明加锁失败 3.解锁:执行delete语句完全利用DB现有能力,实现简单1.锁无超时自动失效机制,有死锁风险 2.不支持锁重入,不支持阻塞等待 3.操作数据库开销大,性能不高
基于数据库基于MongoDB findAndModify原子操作1.加锁:执行findAndModify原子命令查找document,若不存在则新增 2.解锁:删除document实现也很容易,较基于MySQL唯一索引的方案,性能要好很多1.大部分公司数据库用MySQL,可能缺乏相应的MongoDB运维、开发人员 2.锁无超时自动失效机制
基于分布式协调系统基于ZooKeeper1.加锁:在/lock目录下创建临时有序节点,判断创建的节点序号是否最小。若是,则表示获取到锁;否,则则watch /lock目录下序号比自身小的前一个节点 2.解锁:删除节点1.由zk保障系统高可用 2.Curator框架已原生支持系列分布式锁命令,使用简单需单独维护一套zk集群,维保成本高
基于缓存基于redis命令1. 加锁:执行setnx,若成功再执行expire添加过期时间 2. 解锁:执行delete命令实现简单,相比数据库和分布式系统的实现,该方案最轻,性能最好1.setnx和expire分2步执行,非原子操作;若setnx执行成功,但expire执行失败,就可能出现死锁 2.delete命令存在误删除非当前线程持有的锁的可能 3.不支持阻塞等待、不可重入
基于缓存基于redis Lua脚本能力1. 加锁:执行SET lock_name random_value EX seconds NX 命令 2. 解锁:执行Lua脚本,释放锁时验证random_value – ARGV[1]为random_value, KEYS[1]为lock_nameif redis.call(“get”, KEYS[1]) == ARGV[1] then return redis.call(“del”,KEYS[1])else return 0end同上;实现逻辑上也更严谨,除了单点问题,生产环境采用用这种方案,问题也不大。不支持锁重入,不支持阻塞等待

表格中对比了几种常见的方案,redis+lua基本可应付工作中分布式锁的需求。然而,redisson分布式锁实现方案(传送门),相比以上方案,redisson保持了简单易用、支持锁重入、支持阻塞等待、Lua脚本原子操作,具有明显的优势。

分布式锁需满足四个条件

首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

  1. 互斥性。在任意时刻,只有一个客户端能持有锁。
  2. 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  3. 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了,即不能误解锁。
  4. 具有容错性。只要大多数Redis节点正常运行,客户端就能够获取和释放锁。

Redisson实现分布式锁

Redisson 是什么?

如果你之前是在用 Redis 的话,那使用 Redisson 的话将会事半功倍,Redisson 提供了使用 Redis 的最简单和最便捷的方法。

Redisson 的宗旨是促进使用者对 Redis 的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。

Redisson 是一个在 Redis 的基础上实现的 Java 驻内存数据网格(In-Memory Data Grid)。

img

原理机制
在这里插入图片描述

集成Spring Boot 项目
  1. 引入依赖 【可引入Spring Boot 封装好的starter】

      <!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
            <dependency>
                <groupId>org.redisson</groupId>
                <artifactId>redisson</artifactId>
                <version>3.5.4</version>
            </dependency>
    
  2. 添加配置类

    @Configuration
    public class MyRedissonConfig {
    
        @Bean(destroyMethod = "shutdown")
        public RedissonClient redissonClient(){
            // 创建配置  记得加redis://
            Config config = new Config();
            config.useSingleServer().setAddress("redis://127.0.0.1:6379");
            // 根据配置创建RedissClient客户端
            RedissonClient redissonClient = Redisson.create(config);
            return redissonClient;
        }
    }
    
基于 AOP 的 Redis 分布式锁

在实际的使用过程中,分布式锁可以封装好后使用在方法级别,这样就不用每个地方都去获取锁和释放锁,使用起来更加方便。

首先定义个注解:

package com.tom.lock.redisson.spring.boot.autoconfigure;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

import org.springframework.core.annotation.AliasFor;

/**
 * @author handsometong
 * @date 2022年11月14日 下午3:10:36
 * @version 1.0.0
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface LockAction {

	/** 锁的资源,key。支持spring El表达式*/
	@AliasFor("key")
	String value() default "'default'";
	
	@AliasFor("value")
	String key() default "'default'";
	
	/** 锁类型*/
	LockType lockType() default LockType.REENTRANT_LOCK;
	
	/** 获取锁等待时间,默认3秒*/
	long waitTime() default 3000L;
	
	/** 锁自动释放时间,默认30秒*/
	long leaseTime() default 30000L;
	
	/** 时间单位(获取锁等待时间和持锁时间都用此单位)*/
	TimeUnit unit() default TimeUnit.MILLISECONDS;
}

定义切面(spring boot配置方式)

package com.tom.lock.redisson.spring.boot.autoconfigure;

import java.lang.reflect.Method;

import com.tom.redisson.spring.boot.autoconfigure.RedissonAutoConfiguration;
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.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

/**
 * @author handsometong
 * @date 2022年11月14日 下午3:11:22
 * @version 1.0.0
 */
@Aspect
@Configuration
@ConditionalOnBean(RedissonClient.class)
@AutoConfigureAfter(RedissonAutoConfiguration.class)
public class RedissonDistributedLockAspectConfiguration {
	
	private final Logger logger = LoggerFactory.getLogger(RedissonDistributedLockAspectConfiguration.class);
	
	@Autowired
	private RedissonClient redissonClient;

	private ExpressionParser parser = new SpelExpressionParser();

	private LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();

	@Pointcut("@annotation(com.tom.lock.redisson.spring.boot.autoconfigure.LockAction)")
	private void lockPoint(){
		
	}
	
	@Around("lockPoint()")
	public Object around(ProceedingJoinPoint pjp) throws Throwable{
		Method method = ((MethodSignature) pjp.getSignature()).getMethod();
		LockAction lockAction = method.getAnnotation(LockAction.class);
		String key = lockAction.value();
		Object[] args = pjp.getArgs();
		key = parse(key, method, args);
		
		RLock lock = getLock(key, lockAction);
		if(!lock.tryLock(lockAction.waitTime(), lockAction.leaseTime(), lockAction.unit())) {
			logger.debug("get lock failed [{}]", key);
			return null;
		}
		
		//得到锁,执行方法,释放锁
		logger.debug("get lock success [{}]", key);
		try {
			return pjp.proceed();
		} catch (Exception e) {
			logger.error("execute locked method occured an exception", e);
		} finally {
			lock.unlock();
			logger.debug("release lock [{}]", key);
		}
		return null;
	}

	/**
	 * @description 解析spring EL表达式
	 * @author handsometong
	 * @date 2022年11月9日 上午10:41:01
	 * @version 1.0.0
	 * @param key 表达式
	 * @param method 方法
	 * @param args 方法参数
	 * @return
	 */
	private String parse(String key, Method method, Object[] args) {
		String[] params = discoverer.getParameterNames(method);
		EvaluationContext context = new StandardEvaluationContext();
		for (int i = 0; i < params.length; i ++) {
			context.setVariable(params[i], args[i]);
		}
		return parser.parseExpression(key).getValue(context, String.class);
	}
	
	private RLock getLock(String key, LockAction lockAction) {
		switch (lockAction.lockType()) {
			case REENTRANT_LOCK:
				return redissonClient.getLock(key);
			
			case FAIR_LOCK:
				return redissonClient.getFairLock(key);
				
			case READ_LOCK:
				return redissonClient.getReadWriteLock(key).readLock();
			
			case WRITE_LOCK:
				return redissonClient.getReadWriteLock(key).writeLock();
				
			default:
				throw new RuntimeException("do not support lock type:" + lockAction.lockType().name());
			}
	}
}

spring boot starter还需要在 resources/META-INF 中添加 spring.factories 文件

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.tom.lock.redisson.spring.boot.autoconfigure.RedissonDistributedLockAspectConfiguration
使用方法

直接在方法上增加 @LockAction注解 (支持spring El表达式)**

	LockAction("'test'.concat(#user.id)")
	public void update(UserVO user){
		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			logger.error("exp", e);
		}
	}

请添加图片描述

源码地址 :https://github.com/handsometong/redission-lock.git
redis分布式锁源码:https://github.com/handsometong/redis-lock

参考文献:

Redis分布式锁-这一篇全了解(Redission实现分布式锁完美方案)

Redis 分布式锁|从青铜到钻石的五种演进方案

分布式锁中的王者方案 - Redisson

关于Redis分布式锁这一篇应该是讲的最好的了,先收藏起来再看!

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值