前言
最近开发公司的项目,遇到了分布式的场景,即,同一条数据可能被多台服务器或者说多个线程同时修改,此时可能会出现分布式事务的问题,随即封装了redis分布式锁的注解。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锁dom
@RedisLock public void updateData( Data param){ }
下面详细分析一下封装的源码:分布式
先看一下项目结构(总共就4个类):ide
//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<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; }
}ui