资讯专栏INFORMATION COLUMN

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

oujie / 3145人阅读

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

前言

最近开发公司的项目,遇到了分布式的场景,即,同一条数据可能被多台服务器或者说多个线程同时修改,此时可能会出现分布式事务的问题,随即封装了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 {
                //这里sleep 10ms是为了防止线程饥饿,各位可以思考一下为什么
                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 beginTime = new ThreadLocal<>();
ThreadLocal objectId = new ThreadLocal<>();
ThreadLocal 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 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 getKeyAndValue(Object obj) {
    Map map = new HashMap();
    // 得到类对象
    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()+"	是String"); f.set(obj,"12") ;
         * //给属性设值 }else if(type.endsWith("int") ||
         * type.endsWith("Integer")){
         * System.out.println(f.getType()+"	是int"); f.set(obj,12) ; //给属性设值
         * }else{ System.out.println(f.getType()+"	"); }
         */

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

}

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/71742.html

相关文章

  • Redis详解 - SpringBoot整合Redis,RedisTemplate和注解两种方式的使

    摘要:和注解的方法返回值要一致删除缓存在需要删除缓存的方法上加注解,执行完这个方法之后会将中对应的记录删除。代表返回值,意思是当返回码不等于时不缓存,也就是等于时才缓存。返回值特定值如果被设置了如果没有被设置例子自动将对应到并且返回原来对应的。 本文主要讲 Redis 的使用,如何与 SpringBoot 项目整合,如何使用注解方式和 RedisTemplate 方式实现缓存。最后会给一个用...

    SexySix 评论0 收藏0
  • 基于 Redis 的布式

    摘要:首先谈到分布式锁自然也就联想到分布式应用。如基于的唯一索引。基于的临时有序节点。这里主要基于进行讨论。该命令可以保证的原子性。所以最好的方式是在每次解锁时都需要判断锁是否是自己的。总结至此一个基于的分布式锁完成,但是依然有些问题。 showImg(https://segmentfault.com/img/remote/1460000014128437?w=2048&h=1365); 前...

    fasss 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<