在集群系统中,有些资源的调用需要加锁,由于服务是有多个资源对外提供的,资源之间的锁使用JAVA的锁是不可行的,所以需要使用其他的方式来实现加锁,比如订单与库存的锁。
redis是基于内存的key-value的NOSQL数据库,效率高,单机并发可达1K左右,其中setnx可以很方便实现并发锁机制
下面是基于SpringBoot以及redis实现的分布式锁的工具类,可以在需要加锁的地方加上注解即可灵活使用
/**
* 该注解(@MethodLock)可以用在方法 上表示给发放加分布式锁
* 与
* @author Administrator
*
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LockMethod {
/**
* 锁的key
* 本参数是必写选项<br/>
*/
String preKey() default "redisLock";
/**
* 持锁时间,超时时间,持锁超过此时间自动丢弃锁<br/>
* 单位毫秒,默认20秒<br/>
* 如果为0表示永远不释放锁,知道过程执行完自动释放锁,
* 在设置为0的情况下toWait为true是没有意义的<br/>
* 但是没有比较强的业务要求下,不建议设置为0
*/
int expireTime() default 20 ;
/**
* 当获取锁失败,是继续等待还是放弃<br/>
* 默认为继续等待
*/
boolean toWait() default true;
// /**
// * 没有获取到锁的情况下且toWait()为继续等待,睡眠指定毫秒数继续获取锁,也就是轮训获取锁的时间<br/>
// * 默认为10毫秒
// *
// * @return
// */
// long sleepMills() default 10;
/**
* 锁获取超时时间:<br/>
* 没有获取到锁的情况下且toWait()为true继续等待,最大等待时间,如果超时抛出
* {@link java.util.concurrent.TimeoutException.TimeoutException}
* ,可捕获此异常做相应业务处理;<br/>
* 单位毫秒,默认3秒钟,如果设置为0即为没有超时时间,一直获取下去;
*
* @return
*/
long maxSleepMills() default 3 * 1000;
}
@Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LockParameter {
/**
* 含有成员变量的复杂对象中需要加锁的成员变量,如一个商品对象的商品ID
* 也就是复杂对象中的属性名称
* 比如User user 即为复杂对象 使用对象中的属性 username来作为sunKey
* isObject=false 时候该功能才起作用
* @return
*/
String field() default "";
/**
* 是否是基本的简单属性 true表示是简单属性,false 表示是复杂对象
* true:表示 int,long,String,char 等基本数据类型
* false:表示对象模型 比如User
* @return
*/
boolean isSimple() default true;
}
@Aspect
@Component
public class RedisLockAop {
// @Before("@annotation(CacheLock)")
// public void requestLimit(JoinPoint point, CacheLock cacheLock)throws Throwable{
//
// }
private static Logger logger = LoggerFactory.getLogger(RedisLockAop.class);
@Autowired
private SpringJedisUtilInterface redisUtil;
// @Autowired
// private RedisUtil redisUtil;
/**
* 通过前置拦截,拦截所有经过CacheLock注解过的方法
* @param point
* @param CacheLock
* @throws Throwable
*/
@Around("@annotation(LockMethod)")
public Object requestLimit(ProceedingJoinPoint joinPoint) throws Throwable{
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
LockMethod methodLock = method.getAnnotation(LockMethod.class);
if(methodLock ==null){
throw new RedisLockException("配置参数错误");
}
Object[] args = joinPoint.getArgs();
// 得到被代理的方法
//获得方法中参数的注解
Annotation[][] annotations = method.getParameterAnnotations();
//根据获取到的参数注解和参数列表获得加锁的参数
String sunKey = getLockEndKey(annotations,args);
String key = methodLock.preKey()+"_"+sunKey+"_lock";
logger.info(key);
// 超时时间 (最大的等待时间)
long maxSleepMills = methodLock.maxSleepMills();
// 获得锁之后最大可持有锁的时间
int expire = methodLock.expireTime();
// 是否等待
boolean toWait = methodLock.toWait();
boolean lock = false;
Object obj = null;
int radomInt = 0;// 每个线程随机等待时间
try {
// 说明没有得到锁,等待轮训的去获得锁
while (!lock) {
lock =getLock(key, expire);
// 得到锁,没有人加过相同的锁
if (lock) {
logger.info("线程获得锁开始执行"+"---"+redisUtil);
obj = joinPoint.proceed();
break;
}else if(toWait){
radomInt = new Random().nextInt(99);
maxSleepMills =maxSleepMills- radomInt;
logger.info("没获得锁,随机等待毫秒数:"+radomInt+"---"+redisUtil);
if(maxSleepMills<=0){
logger.info("获取锁资源等待超时"+"---"+redisUtil);
break;
// throw new CacheLockException("获取锁资源等待超时,等待超时");
}
TimeUnit.MILLISECONDS.sleep(radomInt);
}else{
break;
}
}
} catch (Throwable e) {
e.printStackTrace();
logger.info(e.getMessage()+"--"+e);
throw e;
}finally{
if(lock){
logger.info("执行删除操作");
redisUtil.del(key );//直接删除
}
}
return obj;
}
/**
* 从方法参数中找出@lockedComplexOnbject的参数,在redis中取该参数对应的锁
* @param annotations
* @param args
* @return
* @throws RedisLockException
*/
private String getLockEndKey(Annotation[][] annotations,Object[] args) throws RedisLockException{
if(null == args || args.length == 0){
throw new RedisLockException("方法参数为空,没有被锁定的对象");
}
if(null == annotations || annotations.length == 0){
throw new RedisLockException("没有被注解的参数");
}
SortedMap<Integer, String> keys = new TreeMap<Integer, String>();
String keyString;
//直接在多个参数上注解
for(int i = 0;i < annotations.length;i++){
for(int j = 0;j < annotations[i].length;j++){
if(annotations[i][j] instanceof Value){
Object arg = args[i];
logger.info("--Value args[i]--"+i+"------"+arg);
}
if(annotations[i][j] instanceof LockParameter){//注解为LockedComplexObject
LockParameter lockParameter = (LockParameter)annotations[i][j];
Object arg = args[i];
logger.info("--lockParameter args[i]--"+i+"------"+arg);
try {
if(lockParameter.isSimple()){
keys.put(i, String.valueOf(arg));
}else{
keyString = args[i].getClass().getField(lockParameter.field()).toString();
keys.put(i,keyString);
}
} catch (NoSuchFieldException |SecurityException e) {
e.printStackTrace();
throw new RedisLockException("注解对象中没有该属性"+lockParameter.field());
}
}
}
}
String sunKey ="";
if (keys != null && keys.size() > 0) {
for (String key : keys.values()) {
sunKey = sunKey + key;
}
}
return sunKey;
}
public boolean getLock(String key ,int expire) throws InterruptedException{
if(redisUtil.setnx(key, String.valueOf(expire))==1){//1插入成功且key不存在,0未插入,key存在
redisUtil.expired(key, expire);
return true;
}
return false;
}
}
测试用例
@Service
public class SecKillImpl implements SeckillInterface{
public static Map<Long, Long> inventory ;
static{
inventory = new HashMap<>();
inventory.put(10000001L, 10000l);
inventory.put(10000002L, 10000l);
}
@Override
@LockMethod(preKey="Seckill",expireTime=1)
public void secKill(String arg1, @LockParameter Long arg2, int a) {
//最简单的秒杀,这里仅作为demo示例
System.out.println("commodityId"+arg2+" 线程几号="+a);
reduceInventory(arg2);
}
//模拟秒杀操作,姑且认为一个秒杀就是将库存减一,实际情景要复杂的多
public Long reduceInventory(Long commodityId){
inventory.put(commodityId,inventory.get(commodityId) - 1);
return inventory.get(commodityId);
}
@RunWith(SpringRunner.class)
@SpringBootTest(classes = FirstSpringBootApplication.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class SecKillTest {
private static Long commidityId1 = 10000001L;
private static Long commidityId2 = 10000002L;
@Autowired
private SeckillInterface proxy;
@Test
public void testSecKill(){
int threadCount = 1000;
int splitPoint = 500;
CountDownLatch endCount = new CountDownLatch(threadCount);
CountDownLatch beginCount = new CountDownLatch(1);
SecKillImpl testClass = new SecKillImpl();
Thread[] threads = new Thread[threadCount];
//起500个线程,秒杀第一个商品
for(int i= 0;i < splitPoint;i++){
int a = i;
threads[i] = new Thread(new Runnable() {
public void run() {
try {
//等待在一个信号量上,挂起
beginCount.await();
//用动态代理的方式调用secKill方法
proxy.secKill("test", commidityId1,a);
endCount.countDown();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
threads[i].start();
}
for(int i= splitPoint;i < threadCount;i++){
int a =i;
threads[i] = new Thread(new Runnable() {
public void run() {
try {
//等待在一个信号量上,挂起
beginCount.await();
//用动态代理的方式调用secKill方法
beginCount.await();
proxy.secKill("test", commidityId2,a );
//testClass.testFunc("test", 10000001L);
endCount.countDown();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
threads[i].start();
}
long startTime = System.currentTimeMillis();
//主线程释放开始信号量,并等待结束信号量
beginCount.countDown();
try {
//主线程等待结束信号量
endCount.await();
//观察秒杀结果是否正确
System.out.println(SecKillImpl.inventory.get(commidityId1));
System.out.println(SecKillImpl.inventory.get(commidityId2));
// System.out.println("error count" + CacheLockInterceptor.ERROR_COUNT);
System.out.println("total cost " + (System.currentTimeMillis() - startTime));
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
以下是项目下载地址:项目中包含了redis并发锁,redis 的session共享 分布式缓存 druid数据库连接池,druid监控等
源码下载地址:https://git.oschina.net/xufan0711/firstSpringboot/tree/master/