自定义注解@RedisLock分布式锁用法及原理

Java框架

浏览数:362

2018-10-17

前言

最近开发公司的项目,遇到了分布式的场景,即,同一条数据可能被多台服务器或者说多个线程同时修改,此时可能会出现分布式事务的问题,随即封装了redis分布式锁的注解。

场景分析

前提:我的银行卡有0元钱,现在有A,B两个人,想分别给我转10元钱
分析:
假如A,B通过读数据库,同时发现我的余额是0,这时,
线程A,会给我设置:
余额 = 10 + 0
线程B,会给我设置:
余额 = 10 + 0

最后,我的卡上收到了两个人的转账,但是最后金额居然只有10元!!这是怎么回事?
其实原因就在于多个线程,对一条数据同时进行了操作。如果我们可以设置一下,在修改的方法上面加一个锁,每次修改之前,(A)先拿到这个锁,再去做修改方法,此时,其他(B)线程想要修改的时候,看到锁已经不再,需要等待锁释放,然后再去执行,就保证了A,B先后依此执行,数据依此累加就没问题了。

解决办法

基于代码的可移植性,我将分布式锁做成了注解,大家如果有需要,可以直接将jar包拿过去做相应的修改即可,jar包下载地址(链接:https://pan.baidu.com/s/1hBn-…
提取码:1msl):

注解使用说明:
1.在需要添加分布式锁的方法上面加上@RedisLock
如果key不添加,则默认锁方法第一个参数param的id字段,如果需要指定锁某个字段,则@RedisLock(key = “code”)
2.如果方法没有参数,则不可使用RedisLock锁

 @RedisLock
public void updateData( Data param){
    
}

下面详细分析一下封装的源码:

先看一下项目结构(总共就4个类):

  //RedisLock注解类:没什么好解释的

  /**
 * Created by liuliang on 2018/10/15.
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisLock {
    //被锁的数据的id
     String key() default "";

     //唤醒时间
     long acquireTimeout() default 6000L;

     //超时时间
     long timeout() default 6000L;
}
 //----------------------类分割线---------------------
//RedisService 一个简单的操作redis的类,封装了加锁和释放锁的方法

/**
 * Created by liuliang on 2018/10/15.
 */
@Service
public class RedisService {
@Autowired
StringRedisTemplate stringRedisTemplate;
@Resource(name = "stringRedisTemplate")
@Autowired
ValueOperations valOpsStr;

@Autowired
RedisTemplate redisTemplate;

@Resource(name = "redisTemplate")
ValueOperations valOpsObj;

public String getStr(String key) {
    return stringRedisTemplate.opsForValue().get(key);//获取对应key的value
    //        return valOpsStr.get(key);
}

public void setStr(String key, String val) {
    stringRedisTemplate.opsForValue().set(key,val,1800, TimeUnit.SECONDS);
    //        valOpsStr.set(key, val);
}

public void del(String key) {
    stringRedisTemplate.delete(key);
}


/**
 * 根据指定o获取Object
 *
 * @param o
 * @return
 */
public Object getObj(Object o) {
    return valOpsObj.get(o);
}

/**
 *       * 设置obj缓存
 *       * @param o1
 *       * @param o2
 *
 */
public void setObj(Object o1, Object o2) {
    valOpsObj.set(o1, o2);
}

/**
 * 删除Obj缓存
 *
 * @param o
 */
public void delObj(Object o) {
    redisTemplate.delete(o);
}


private static JedisPool pool = null;

static {
    JedisPoolConfig config = new JedisPoolConfig();
    // 设置最大连接数
    config.setMaxTotal(200);
    // 设置最大空闲数
    config.setMaxIdle(8);
    // 设置最大等待时间
    config.setMaxWaitMillis(1000 * 100);
    // 在borrow一个jedis实例时,是否需要验证,若为true,则所有jedis实例均是可用的
    config.setTestOnBorrow(true);
    pool = new JedisPool(config, "127.0.0.1", 6379, 3000);
}


DistributedLock lock = new DistributedLock(pool);

/**
 * redis分布式加锁
 * @param objectId
 * @param acquireTimeout
 * @param timeout
 */
public String redisLock(String objectId,Long acquireTimeout, Long timeout) {
    // 对key为id加锁, 返回锁的value值,供释放锁时候进行判断
    String lockValue = lock.lockWithTimeout(objectId, acquireTimeout, timeout);
    System.out.println(Thread.currentThread().getName() + "获得了锁");
    return lockValue;
}


/**
 * 释放redis分布式锁
 * @param objectId
 * @param lockValue
 */
public Boolean  releaseLock(String objectId,String lockValue){
    boolean b = lock.releaseLock(objectId, lockValue);
    System.out.println(Thread.currentThread().getName() + "释放了锁");
   return b;
}



 //----------------------类分割线---------------------
/**
 * Created by liuliang on 2018/10/15.
 *
 * 分布式锁的主要类,主要方法就是加锁和释放锁
 *具体的逻辑在代码注释里面写的很清楚了
 */
@Slf4j
public class DistributedLock {
private final JedisPool jedisPool;

public DistributedLock(JedisPool jedisPool) {
    this.jedisPool = jedisPool;
}


/**
 * 加锁
 * @param locaName  锁的key
 * @param acquireTimeout  获取超时时间
 * @param timeout   锁的超时时间
 * @return 锁标识
 */
public String lockWithTimeout(String locaName,
                              long acquireTimeout, long timeout) {
    Jedis conn = null;
    String retIdentifier = null;
    try {
        // 获取连接
        conn = jedisPool.getResource();
        // 随机生成一个value
        String identifier = UUID.randomUUID().toString();
        // 锁名,即key值
        String lockKey = "lock:" + locaName;
        // 超时时间,上锁后超过此时间则自动释放锁
        int lockExpire = (int)(timeout / 1000);

        // 获取锁的超时时间,超过这个时间则放弃获取锁
        long end = System.currentTimeMillis() + acquireTimeout;
        while (System.currentTimeMillis() < end) {
            log.info("lock...lock...");
            if (conn.setnx(lockKey, identifier) == 1) {
                log.info("==============lock success!=============");
                conn.expire(lockKey, lockExpire);
                // 返回value值,用于释放锁时间确认
                retIdentifier = identifier;
                return retIdentifier;
            }
            // 返回-1代表key没有设置超时时间,为key设置一个超时时间
            if (conn.ttl(lockKey) == -1) {
                conn.expire(lockKey, lockExpire);
            }

            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    } catch (JedisException e) {
        e.printStackTrace();
    } finally {
        if (conn != null) {
            conn.close();
        }
    }
    return retIdentifier;
}


/**
 * 释放锁
 * @param lockName 锁的key
 * @param identifier    释放锁的标识
 * @return
 */
public boolean releaseLock(String lockName, String identifier) {
    Jedis conn = null;
    String lockKey = "lock:" + lockName;
    boolean retFlag = false;
    try {
        conn = jedisPool.getResource();
        while (true) {
            // 监视lock,准备开始事务
            conn.watch(lockKey);
            //避免空指针
            String lockKeyValue = conn.get(lockKey)==null?"":conn.get(lockKey);
            // 通过前面返回的value值判断是不是该锁,若是该锁,则删除,释放锁
            if (lockKeyValue.equals(identifier)) {
                Transaction transaction = conn.multi();
                transaction.del(lockKey);
                List results = transaction.exec();
                if (results == null) {
                    continue;
                }
                log.info("==============unlock success!=============");
                retFlag = true;
            }
            conn.unwatch();
            break;
        }
    } catch (JedisException e) {
        e.printStackTrace();
    } finally {
        if (conn != null) {
            conn.close();
        }
    }
    return retFlag;
}
 //----------------------类分割线---------------------
/**
 * Created by liuliang on 2018/10/16.
 这是一个拦截器,我们指定拦截RedisLock注解
 */
@Aspect
@Component
@Slf4j
public class RedisLockAop {

ThreadLocal<Long> beginTime = new ThreadLocal<>();
ThreadLocal<String> objectId = new ThreadLocal<>();
ThreadLocal<String> lockValue = new ThreadLocal<>();

@Autowired
private RedisService redisService;

@Pointcut("@annotation(redisLock)")
public void serviceStatistics(RedisLock redisLock) {
}

@Before("serviceStatistics(redisLock)")
public void doBefore(JoinPoint joinPoint, RedisLock redisLock) {
    // 记录请求到达时间
    beginTime.set(System.currentTimeMillis());
    //注解所在方法名
    String methodName = joinPoint.getSignature().getName();
    //注解所在类
    String className = joinPoint.getSignature().getDeclaringTypeName();
    //方法上的参数
    Object[] args = joinPoint.getArgs();
    String key = redisLock.key();
    if(ObjectUtils.isNullOrEmpty(args)){
        //方法的参数是空,生成永远不重复的uuid,相当于不做控制
        key = methodName +  UUID.randomUUID().toString();
    }else {
        //取第一个参数指定字段,若没有指定,则取id字段
        Object arg = args[0];
        log.info("arg:"+arg.toString());
        Map<String, Object> map = getKeyAndValue(arg);
        Object o = map.get(StringUtils.isEmpty(key) ? "id" : key);
        if(ObjectUtils.isNullOrEmpty(o)){
            //自定义异常,可以换成自己项目的异常
            throw new MallException(RespCode.REDIS_LOCK_KEY_NULL);
        }
        key = o.toString();
    }
    log.info("线程:"+Thread.currentThread().getName() + ", 已进入方法:"+className+"."+methodName);
//        objectId.set(StringUtils.isEmpty(redisLock.key()) ? UserUtils.getCurrentUser().getId() : redisLock.key());
    objectId.set(key);
    String lock = redisService.redisLock(objectId.get(), redisLock.acquireTimeout(), redisLock.timeout());
    lockValue.set(lock);
    log.info("objectId:"+objectId.get()+",lockValue:"+lock +",已经加锁!");
}


@After("serviceStatistics(redisLock)")
public void doAfter(JoinPoint joinPoint,RedisLock redisLock) {
    String methodName = joinPoint.getSignature().getName();
    String className = joinPoint.getSignature().getDeclaringTypeName();
    redisService.releaseLock(objectId.get(),lockValue.get());
    log.info("objectId:"+objectId.get()+",lockValue:"+lockValue.get() +",已经解锁!");
    log.info("线程:"+Thread.currentThread().getName() + ", 已退出方法:"+className+"."+methodName+",耗时:"+(System.currentTimeMillis() - beginTime.get() +" 毫秒!"));
}


//这是一个Object转mapd的方法
public static Map<String, Object> getKeyAndValue(Object obj) {
    Map<String, Object> map = new HashMap<String, Object>();
    // 得到类对象
    Class userCla = (Class) obj.getClass();
    /* 得到类中的所有属性集合 */
    Field[] fs = userCla.getDeclaredFields();
    for (int i = 0; i < fs.length; i++) {
        Field f = fs[i];
        f.setAccessible(true); // 设置些属性是可以访问的
        Object val = new Object();
        try {
            val = f.get(obj);
            // 得到此属性的值
            map.put(f.getName(), val);// 设置键值
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

        /*
         * String type = f.getType().toString();//得到此属性的类型 if
         * (type.endsWith("String")) {
         * System.out.println(f.getType()+"\t是String"); f.set(obj,"12") ;
         * //给属性设值 }else if(type.endsWith("int") ||
         * type.endsWith("Integer")){
         * System.out.println(f.getType()+"\t是int"); f.set(obj,12) ; //给属性设值
         * }else{ System.out.println(f.getType()+"\t"); }
         */

    }
    System.out.println("单个对象的所有键值==反射==" + map.toString());
    return map;
}