摘要:前言最近開發公司的項目,遇到了分布式的場景,即,同一條數據可能被多臺服務器或者說多個線程同時修改,此時可能會出現分布式事務的問題,隨即封裝了分布式鎖的注解。
前言
最近開發公司的項目,遇到了分布式的場景,即,同一條數據可能被多臺服務器或者說多個線程同時修改,此時可能會出現分布式事務的問題,隨即封裝了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 { ThreadLocalbeginTime = 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; }
}
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/71742.html
摘要:和注解的方法返回值要一致刪除緩存在需要刪除緩存的方法上加注解,執行完這個方法之后會將中對應的記錄刪除。代表返回值,意思是當返回碼不等于時不緩存,也就是等于時才緩存。返回值特定值如果被設置了如果沒有被設置例子自動將對應到并且返回原來對應的。 本文主要講 Redis 的使用,如何與 SpringBoot 項目整合,如何使用注解方式和 RedisTemplate 方式實現緩存。最后會給一個用...
摘要:首先談到分布式鎖自然也就聯想到分布式應用。如基于的唯一索引?;诘呐R時有序節點。這里主要基于進行討論。該命令可以保證的原子性。所以最好的方式是在每次解鎖時都需要判斷鎖是否是自己的??偨Y至此一個基于的分布式鎖完成,但是依然有些問題。 showImg(https://segmentfault.com/img/remote/1460000014128437?w=2048&h=1365); 前...
閱讀 3474·2021-09-22 15:02
閱讀 3520·2021-09-02 15:21
閱讀 2139·2019-08-30 15:55
閱讀 2785·2019-08-30 15:44
閱讀 785·2019-08-29 16:56
閱讀 2419·2019-08-23 18:22
閱讀 3347·2019-08-23 12:20
閱讀 3095·2019-08-23 11:28