简单注解实现集群同步锁(spring+redis+注解)

互联网面试的时候,是不是面试官常问一个问题如何保证集群环境下数据操作并发问题,常用的synchronized肯定是无法满足了,或许你可以借助for update对数据加锁。本文的最终解决方式你只要在方法上加一个@P4jSyn注解就能保证集群环境下同synchronized的效果,且锁的key可以任意指定。本注解还支持了锁的超时机制。

本文需要对Redis、spring和spring-data-redis有一定的了解。当然你可以借助本文的思路对通过注解对方法返回数据进行缓存,类似com.google.code.simple-spring-memcached的@ReadThroughSingleCache。

第一步:  介绍两个自定义注解P4jSyn、P4jSynKey

P4jSyn:必选项,标记在方法上,表示需要对该方法加集群同步锁;

P4jSynKey:可选项,加在方法参数上,表示以方法某个参数作为锁的key,用来保证更多的坑,P4jSynKey并不是强制要添加的,当没有P4jSynKey标记的情况下只会以P4jSyn的synKey作为锁key。

package com.yaoguoyin.redis.lock;
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;
/**
 * <b>同步锁:</b><br/>
 * 主要作用是在服务器集群环境下保证方法的synchronize;<br/>
 * 标记在方法上,使该方法的执行具有互斥性,并不保证并发执行方法的先后顺序;<br/>
 * 如果原有“A任务”获取锁后任务执行时间超过最大允许持锁时间,且锁被“B任务”获取到,在“B任务”成功货物锁会并不会终止“A任务”的执行;<br/>
 * <br/>
 * <b>注意:</b><br/>
 * 使用过程中需要注意keepMills、toWait、sleepMills、maxSleepMills等参数的场景使用;<br/>
 * 需要安装redis,并使用spring和spring-data-redis等,借助redis NX等方法实现。
 *
 * @see com.yaoguoyin.redis.lock.P4jSynKey
 * @see com.yaoguoyin.redis.lock.RedisLockAspect
 *
 * @author partner4java
 *
 */
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface P4jSyn {
 /**
 * 锁的key<br/>
 * 如果想增加坑的个数添加非固定锁,可以在参数上添加@P4jSynKey注解,但是本参数是必写选项<br/>
 * redis key的拼写规则为 "RedisSyn+" + synKey + @P4jSynKey<br/>
 *
 */
 String synKey();
 /**
 * 持锁时间,超时时间,持锁超过此时间自动丢弃锁<br/>
 * 单位毫秒,默认20秒<br/>
 * 如果为0表示永远不释放锁,在设置为0的情况下toWait为true是没有意义的<br/>
 * 但是没有比较强的业务要求下,不建议设置为0
 */
 long keepMills() default 20 * 1000;
 /**
 * 当获取锁失败,是继续等待还是放弃<br/>
 * 默认为继续等待
 */
 boolean toWait() default true;
 /**
 * 没有获取到锁的情况下且toWait()为继续等待,睡眠指定毫秒数继续获取锁,也就是轮训获取锁的时间<br/>
 * 默认为10毫秒
 *
 * @return
 */
 long sleepMills() default 10;
 /**
 * 锁获取超时时间:<br/>
 * 没有获取到锁的情况下且toWait()为true继续等待,最大等待时间,如果超时抛出
 * {@link java.util.concurrent.TimeoutException.TimeoutException}
 * ,可捕获此异常做相应业务处理;<br/>
 * 单位毫秒,默认一分钟,如果设置为0即为没有超时时间,一直获取下去;
 *
 * @return
 */
 long maxSleepMills() default 60 * 1000;
} 
package com.yaoguoyin.redis.lock;
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;
/**
 * <b>同步锁 key</b><br/>
 * 加在方法的参数上,指定的参数会作为锁的key的一部分
 *
 * @author partner4java
 *
 */
@Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface P4jSynKey {
 /**
 * key的拼接顺序
 *
 * @return
 */
 int index() default 0;
} 

这里就不再对两个注解进行使用上的解释了,因为注释已经说明的很详细了。

使用示例:

package com.yaoguoyin.redis.lock;
import org.springframework.stereotype.Component;
@Component
public class SysTest {
 private static int i = 0;
 @P4jSyn(synKey = "12345")
 public void add(@P4jSynKey(index = 1) String key, @P4jSynKey(index = 0) int key1) {
 i++;
 System.out.println("i=-===========" + i);
 }
} 

第二步:切面编程

在不影响原有代码的前提下,保证执行同步,目前最直接的方式就是使用切面编程

package com.yaoguoyin.redis.lock;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.RedisTemplate;
/**
 * 锁的切面编程<br/>
 * 针对添加@RedisLock 注解的方法进行加锁
 *
 * @see com.yaoguoyin.redis.lock.P4jSyn
 *
 * @author partner4java
 *
 */
@Aspect
public class RedisLockAspect {
 @Autowired
 @Qualifier("redisTemplate")
 private RedisTemplate<String, Long> redisTemplate;
 @Around("execution(* com.yaoguoyin..*(..)) && @annotation(com.yaoguoyin.redis.lock.P4jSyn)")
 public Object lock(ProceedingJoinPoint pjp) throws Throwable {
 P4jSyn lockInfo = getLockInfo(pjp);
 if (lockInfo == null) {
  throw new IllegalArgumentException("配置参数错误");
 }
 String synKey = getSynKey(pjp, lockInfo.synKey());
 if (synKey == null || "".equals(synKey)) {
  throw new IllegalArgumentException("配置参数synKey错误");
 }
 boolean lock = false;
 Object obj = null;
 try {
  // 超时时间
  long maxSleepMills = System.currentTimeMillis() + lockInfo.maxSleepMills();
  while (!lock) {
  long keepMills = System.currentTimeMillis() + lockInfo.keepMills();
  lock = setIfAbsent(synKey, keepMills);
  // 得到锁,没有人加过相同的锁
  if (lock) {
   obj = pjp.proceed();
  }
  // 锁设置了没有超时时间
  else if (lockInfo.keepMills() <= 0) {
   // 继续等待获取锁
   if (lockInfo.toWait()) {
   // 如果超过最大等待时间抛出异常
   if (lockInfo.maxSleepMills() > 0 && System.currentTimeMillis() > maxSleepMills) {
    throw new TimeoutException("获取锁资源等待超时");
   }
   TimeUnit.MILLISECONDS.sleep(lockInfo.sleepMills());
   } else {
   break;
   }
  }
  // 已过期,并且getAndSet后旧的时间戳依然是过期的,可以认为获取到了锁
  else if (System.currentTimeMillis() > getLock(synKey) && (System.currentTimeMillis() > getSet(synKey, keepMills))) {
   lock = true;
   obj = pjp.proceed();
  }
  // 没有得到任何锁
  else {
   // 继续等待获取锁
   if (lockInfo.toWait()) {
   // 如果超过最大等待时间抛出异常
   if (lockInfo.maxSleepMills() > 0 && System.currentTimeMillis() > maxSleepMills) {
    throw new TimeoutException("获取锁资源等待超时");
   }
   TimeUnit.MILLISECONDS.sleep(lockInfo.sleepMills());
   }
   // 放弃等待
   else {
   break;
   }
  }
  }
 } catch (Exception e) {
  e.printStackTrace();
  throw e;
 } finally {
  // 如果获取到了锁,释放锁
  if (lock) {
  releaseLock(synKey);
  }
 }
 return obj;
 }
 /**
 * 获取包括方法参数上的key<br/>
 * redis key的拼写规则为 "RedisSyn+" + synKey + @P4jSynKey
 *
 */
 private String getSynKey(ProceedingJoinPoint pjp, String synKey) {
 try {
  synKey = "RedisSyn+" + synKey;
  Object[] args = pjp.getArgs();
  if (args != null && args.length > 0) {
  MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
  Annotation[][] paramAnnotationArrays = methodSignature.getMethod().getParameterAnnotations();
  SortedMap<Integer, String> keys = new TreeMap<Integer, String>(); 

  for (int ix = 0; ix < paramAnnotationArrays.length; ix++) {
   P4jSynKey p4jSynKey = getAnnotation(P4jSynKey.class, paramAnnotationArrays[ix]);
   if (p4jSynKey != null) {
   Object arg = args[ix];
   if (arg != null) {
    keys.put(p4jSynKey.index(), arg.toString());
   }
   }
  }
  if (keys != null && keys.size() > 0) {
   for (String key : keys.values()) {
   synKey = synKey + key;
   }
  }
  }
  return synKey;
 } catch (Exception e) {
  e.printStackTrace();
 }
 return null;
 }
 @SuppressWarnings("unchecked")
 private static <T extends Annotation> T getAnnotation(final Class<T> annotationClass, final Annotation[] annotations) {
 if (annotations != null && annotations.length > 0) {
  for (final Annotation annotation : annotations) {
  if (annotationClass.equals(annotation.annotationType())) {
   return (T) annotation;
  }
  }
 }
 return null;
 }
 /**
 * 获取RedisLock注解信息
 */
 private P4jSyn getLockInfo(ProceedingJoinPoint pjp) {
 try {
  MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
  Method method = methodSignature.getMethod();
  P4jSyn lockInfo = method.getAnnotation(P4jSyn.class);
  return lockInfo;
 } catch (Exception e) {
  e.printStackTrace();
 }
 return null;
 }
 public BoundValueOperations<String, Long> getOperations(String key) {
 return redisTemplate.boundValueOps(key);
 }
 /**
 * Set {@code value} for {@code key}, only if {@code key} does not exist.
 * <p>
 * See http://redis.io/commands/setnx
 *
 * @param key
 *  must not be {@literal null}.
 * @param value
 *  must not be {@literal null}.
 * @return
 */
 public boolean setIfAbsent(String key, Long value) {
 return getOperations(key).setIfAbsent(value);
 }
 public long getLock(String key) {
 Long time = getOperations(key).get();
 if (time == null) {
  return 0;
 }
 return time;
 }
 public long getSet(String key, Long value) {
 Long time = getOperations(key).getAndSet(value);
 if (time == null) {
  return 0;
 }
 return time;
 }
 public void releaseLock(String key) {
 redisTemplate.delete(key);
 }
} 

RedisLockAspect会对添加注解的方法进行特殊处理,具体可看lock方法。

大致思路就是:

1、首选借助redis本身支持对应的setIfAbsent方法,该方法的特点是如果redis中已有该数据不保存返回false,不存该数据保存返回true;

2、如果setIfAbsent返回true标识拿到同步锁,可进行操作,操作后并释放锁;

3、如果没有通过setIfAbsent拿到数据,判断是否对锁设置了超时机制,没有设置判断是否需要继续等待;

4、判断是否锁已经过期,需要对(System.currentTimeMillis() > getLock(synKey) && (System.currentTimeMillis() > getSet(synKey, keepMills)))进行细细的揣摩一下,getSet可能会改变了其他人拥有锁的超时时间,但是几乎可以忽略;

5、没有得到任何锁,判断继续等待还是退出。

第三步:spring的基本配置

#*****************jedis连接参数设置*********************# 

#redis服务器ip #
redis.hostName=127.0.0.1 

#redis服务器端口号#
redis.port=6379 

#redis服务器外部访问密码
redis.password=XXXXXXXXXX 

#************************jedis池参数设置*******************# 

#jedis的最大分配对象#
jedis.pool.maxActive=1000 

jedis.pool.minIdle=100 

#jedis最大保存idel状态对象数 #
jedis.pool.maxIdle=1000 

#jedis池没有对象返回时,最大等待时间 #
jedis.pool.maxWait=5000 

#jedis调用borrowObject方法时,是否进行有效检查#
jedis.pool.testOnBorrow=true 

#jedis调用returnObject方法时,是否进行有效检查 #
jedis.pool.testOnReturn=true 
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xmlns:jee="http://www.springframework.org/schema/jee"xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop"xmlns:redis="http://www.springframework.org/schema/redis" xmlns:cache="http://www.springframework.org/schema/cache" xsi:schemaLocation="http://www.springframework.org/schema/beans  http://www.springframework.org/schema/beans/spring-beans-4.2.xsd  http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd  http://www.springframework.org/schema/aop  http://www.springframework.org/schema/aop/spring-aop-4.1.xsd  http://www.springframework.org/schema/redis  http://www.springframework.org/schema/redis/spring-redis.xsd
http://www.springframework.org/schema/cache  http://www.springframework.org/schema/cache/spring-cache.xsd">
 <!-- 开启注解 -->
 <aop:aspectj-autoproxy />
 <bean class="com.yaoguoyin.redis.lock.RedisLockAspect" />
 <!-- 扫描注解包范围 -->
 <context:component-scan base-package="com.yaoguoyin" />
 <!-- 引入redis配置 -->
 <context:property-placeholder location="classpath:config.properties" />
 <!-- 连接池 -->
 <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
 <property name="minIdle" value="${jedis.pool.minIdle}" />
 <property name="maxIdle" value="${jedis.pool.maxIdle}" />
 <property name="maxWaitMillis" value="${jedis.pool.maxWait}" />
 </bean>
 <!-- p:password="${redis.pass}" -->
 <bean id="redisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" p:host-name="${redis.hostName}" p:port="${redis.port}"
 p:password="${redis.password}" p:pool-config-ref="poolConfig" />
 <!-- 类似于jdbcTemplate -->
 <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate" p:connection-factory-ref="redisConnectionFactory" />
</beans> 

redis的安装本文就不再说明。

测试

package com.yaoguoyin.redis;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:META-INF/spring/redis.xml" })
public class BaseTest extends AbstractJUnit4SpringContextTests {
} 
package com.yaoguoyin.redis.lock;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import com.yaoguoyin.redis.BaseTest;
public class RedisTest extends BaseTest {
 @Autowired
 private SysTest sysTest;
 @Test
 public void testHello() throws InterruptedException {
 for (int i = 0; i < 100; i++) {
  new Thread(new Runnable() {
  @Override
  public void run() {
   try {
   TimeUnit.SECONDS.sleep(1);
   } catch (InterruptedException e) {
   e.printStackTrace();
   }
   sysTest.add("xxxxx", 111111);
  }
  }).start();
 }
 TimeUnit.SECONDS.sleep(20);
 }
 @Test
 public void testHello2() throws InterruptedException{
 sysTest.add("xxxxx", 111111);
 TimeUnit.SECONDS.sleep(10);
 }
} 

你可以对

void com.yaoguoyin.redis.lock.SysTest.add(@P4jSynKey(index=1) String key, @P4jSynKey(index=0) int key1)

去除注解@P4jSyn进行测试对比。

ps:本demo的执行性能取决于redis和Java交互距离;成千山万单锁并发建议不要使用这种形式,直接通过redis等解决,本demo只解决小并发不想耦合代码的形式。

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持我们!

(0)

相关推荐

  • Redis 集群搭建和简单使用教程

    前言 Redis集群搭建的目的其实也就是集群搭建的目的,所有的集群主要都是为了解决一个问题,横向扩展. 在集群的概念出现之前,我们使用的硬件资源都是纵向扩展的,但是纵向扩展很快就会达到一个极限,单台机器的Cpu的处理速度,内存大小,硬盘大小没办法一直满足需求,而且机器纵向扩展的成本是相当高的.集群的出现就是能够让多台机器像一台机器一样工作,实现了资源的横向扩展. Redis是内存型数据库,当我们要存储的数据达到一定程度时,单台机器的内存满足不了我们的需求,搭建集群则是一种很好的解决方案. 介绍安

  • redis集群规范详解

    本文档翻译自 http://redis.io/topics/cluster-spec . 引言 这个文档是正在开发中的 Redis 集群功能的规范(specification)文档, 文档分为两个部分: 第一部分介绍目前已经在 unstable 分支中实现了的那些功能. 第二部分介绍目前仍未实现的那些功能. 文档各个部分的内容可能会随着集群功能的设计修改而发生改变, 其中, 未实现功能发生修改的几率比已实现功能发生修改的几率要高. 这个规范包含了编写客户端库(client library)所需的

  • Windows环境部署Redis集群

    一.准备文件 1.下载Redis for windows 的最新版本 下载地址:https://github.com/MSOpenTech/redis/releases 安装到 c:\Redis 目录下(Redis-x64-3.2.100.msi <Windows服务版>) 2.下载 RubyInstaller 下载地址:http://rubyinstaller.org/downloads/ 安装时,勾选:(所使用版本rubyinstaller-2.3.1-x64.exe) Install T

  • 在redhat6.4安装redis集群【教程】

    参考: http://redis.io/topics/cluster-tutorial(主要是Creating a Redis Cluster using the create-cluster script部分) https://ruby.taobao.org/ 安装一款不熟悉的软件前先看INSTALL,README,这是习惯,生产上要建立普通用户并调节适当参数,下面是以root身份安装运行. 下载解压并安装redis make test提示需要更高版本的tcl,跳到安装过程可能遇到的问题 wg

  • CentOS 7下安装 redis 3.0.6并配置集群的过程详解

    安装依赖 [root@centos7-1 ~]# yum -y install gcc openssl-devel libyaml-devel libffi-devel readline-devel zlib-devel gdbm-devel ncurses-devel gcc-c++ automake autoconf 安装 redis [root@centos7-1 ~]# wget http://download.redis.io/releases/redis-3.0.6.tar.gz [

  • 简单注解实现集群同步锁(spring+redis+注解)

    互联网面试的时候,是不是面试官常问一个问题如何保证集群环境下数据操作并发问题,常用的synchronized肯定是无法满足了,或许你可以借助for update对数据加锁.本文的最终解决方式你只要在方法上加一个@P4jSyn注解就能保证集群环境下同synchronized的效果,且锁的key可以任意指定.本注解还支持了锁的超时机制. 本文需要对Redis.spring和spring-data-redis有一定的了解.当然你可以借助本文的思路对通过注解对方法返回数据进行缓存,类似com.googl

  • 浅谈spring 常用注解

    我们不妨先将spring常用的注解按照功能进行分类 1 .将普通类加入容器形成Bean的注解 日常开发中主要使用到的定义Bean的注解包括(XML方式配置bean暂不讨论): @Component.@Repository.@Service.@Controller.@Bean 其中@Component.@Repository.@Service.@Controller实质上属于同一类注解,用法相同,功能相同,区别在于标识组件的类型.当一个组件代表数据访问层(Dao)时,你可以给它加上@Reposit

  • Spring AOP注解失效的坑及JDK动态代理

    @Transactional @Async等注解不起作用 之前很多人在使用Spring中的@Transactional, @Async等注解时,都多少碰到过注解不起作用的情况. 为什么会出现这些情况呢?因为这些注解的功能实际上都是Spring AOP实现的,而其实现原理是通过代理实现的. JDK动态代理 以一个简单的例子理解一下JDK动态代理的基本原理: //目标类接口 public interface JDKProxyTestService { void run(); } //目标类 publ

  • Spring框架 注解配置事务控制的流程

    目录 基于注解的事务控制 1.配置事务管理器 2.在业务层使用@Transactional 注解 3.开启 spring 对注解事务的支持 4.注解扫描器 Spring 注解事务实现机制 1.事务的实现机制 2.注解方式的事务使用注意事项 写在前面:虽然使用注解方式配置事务控制很简单,用起来也很爽,但是在每个方法前都加上@xxx形式的注解,显然并不美观,也不利于代码的规范与维护,所以XML的配置方式是才是重点. 基于注解的事务控制 基于注解配置事务控制,相较XML配置来说更加简单,但仍需要XML

  • 基于Spring的注解@Qualifier小结

    目录 Spring的注解@Qualifier小结 先说明下场景,代码如下 @qualifier注解 参见下面的例子 Spring的注解@Qualifier小结 近期在捯饬spring的注解,现将遇到的问题记录下来,以供遇到同样问题的童鞋解决~ 先说明下场景,代码如下 有如下接口: public interface EmployeeService { public EmployeeDto getEmployeeById(Long id); } 同时有下述两个实现类 EmployeeServiceI

  • Spring基于注解整合Redis完整实例

    在<Redis之--Spring整合Redis>一文中,向大家介绍了如何将spring与Redis整合起来,但不是基于注解的形式,很多同学都希望能够通过注解的形式来简单的将Spring与Redis整合起来,这样,在使用的时候,只需要在相应的方法上加上注解,便可以使方法轻松的调用Redis的缓存.那么今天就来向大家介绍如何用基于注解的形式来整合Spring与Redis. 一.项目搭建 今天,我们不使用hibernate来操作数据库了,我们今天选择的框架是: Spring4(包括mvc.conte

  • 详解spring mvc(注解)上传文件的简单例子

    spring mvc(注解)上传文件的简单例子. 这有几个需要注意的地方 1.form的enctype="multipart/form-data" 这个是上传文件必须的 2.applicationContext.xml中 <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"/> 关于

  • Spring的注解简单介绍

    SpringMVC的核心组件 DispatcherServlet-–>控制器,请求入口 HanderMapping--->控制器,请求派发 Controller---->控制器,请求处理流程 ModelAndView---->模型,封装业务处理结果和视图 ViewResolver---->视图,视图显示处理器 处理流程 浏览器向Spting发出请求,请求交给前端控制器DispatcherServlet处理. 控制器通过HanderMapping找到相应的Controller组

  • spring boot注解方式使用redis缓存操作示例

    本文实例讲述了spring boot注解方式使用redis缓存操作.分享给大家供大家参考,具体如下: 引入依赖库 在pom中引入依赖库,如下 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> &l

  • Go结合Redis用最简单的方式实现分布式锁

    目录 前言 单Redis实例场景 加解锁示例 小结 多Redis实例场景 加解锁示例 小结 总结 前言 在项目中我们经常有需要使用分布式锁的场景,而Redis是实现分布式锁最常见的一种方式,并且我们也都希望能够把代码写得简单一点,所以今天我们尽量用最简单的方式来实现. 下面的代码使用go-redis客户端和gofakeit,参考和引用了Redis官方文章 单Redis实例场景 如果熟悉Redis的命令,可能会马上想到使用Redis的set if not exists操作来实现,并且现在标准的实现

随机推荐