亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb

首頁 > 數據庫 > Redis > 正文

基于redis分布式鎖實現秒殺功能

2020-03-17 12:35:14
字體:
來源:轉載
供稿:網友

最近在項目中遇到了類似“秒殺”的業務場景,在本篇博客中,我將用一個非常簡單的demo,闡述實現所謂“秒殺”的基本思路。

業務場景

所謂秒殺,從業務角度看,是短時間內多個用戶“爭搶”資源,這里的資源在大部分秒殺場景里是商品;將業務抽象,技術角度看,秒殺就是多個線程對資源進行操作,所以實現秒殺,就必須控制線程對資源的爭搶,既要保證高效并發,也要保證操作的正確。

一些可能的實現

剛才提到過,實現秒殺的關鍵點是控制線程對資源的爭搶,根據基本的線程知識,可以不加思索的想到下面的一些方法:
1、秒殺在技術層面的抽象應該就是一個方法,在這個方法里可能的操作是將商品庫存-1,將商品加入用戶的購物車等等,在不考慮緩存的情況下應該是要操作數據庫的。那么最簡單直接的實現就是在這個方法上加上synchronized關鍵字,通俗的講就是鎖住整個方法;
2、鎖住整個方法這個策略簡單方便,但是似乎有點粗暴??梢陨晕灮幌?,只鎖住秒殺的代碼塊,比如寫數據庫的部分;
3、既然有并發問題,那我就讓他“不并發”,將所有的線程用一個隊列管理起來,使之變成串行操作,自然不會有并發問題。

上面所述的方法都是有效的,但是都不好。為什么?第一和第二種方法本質上是“加鎖”,但是鎖粒度依然比較高。什么意思?試想一下,如果兩個線程同時執行秒殺方法,這兩個線程操作的是不同的商品,從業務上講應該是可以同時進行的,但是如果采用第一二種方法,這兩個線程也會去爭搶同一個鎖,這其實是不必要的。第三種方法也沒有解決上面說的問題。

那么如何將鎖控制在更細的粒度上呢?可以考慮為每個商品設置一個互斥鎖,以和商品ID相關的字符串為唯一標識,這樣就可以做到只有爭搶同一件商品的線程互斥,不會導致所有的線程互斥。分布式鎖恰好可以幫助我們解決這個問題。

何為分布式鎖

分布式鎖是控制分布式系統之間同步訪問共享資源的一種方式。在分布式系統中,常常需要協調他們的動作。如果不同的系統或是同一個系統的不同主機之間共享了一個或一組資源,那么訪問這些資源的時候,往往需要互斥來防止彼此干擾來保證一致性,在這種情況下,便需要使用到分布式鎖。

我們來假設一個最簡單的秒殺場景:數據庫里有一張表,column分別是商品ID,和商品ID對應的庫存量,秒殺成功就將此商品庫存量-1?,F在假設有1000個線程來秒殺兩件商品,500個線程秒殺第一個商品,500個線程秒殺第二個商品。我們來根據這個簡單的業務場景來解釋一下分布式鎖。
通常具有秒殺場景的業務系統都比較復雜,承載的業務量非常巨大,并發量也很高。這樣的系統往往采用分布式的架構來均衡負載。那么這1000個并發就會是從不同的地方過來,商品庫存就是共享的資源,也是這1000個并發爭搶的資源,這個時候我們需要將并發互斥管理起來。這就是分布式鎖的應用。
而key-value存儲系統,如redis,因為其一些特性,是實現分布式鎖的重要工具。

具體的實現

先來看看一些redis的基本命令:
SETNX key value
如果key不存在,就設置key對應字符串value。在這種情況下,該命令和SET一樣。當key已經存在時,就不做任何操作。SETNX是”SET if Not eXists”。
expire KEY seconds
設置key的過期時間。如果key已過期,將會被自動刪除。
del KEY
刪除key
由于筆者的實現只用到這三個命令,就只介紹這三個命令,更多的命令以及redis的特性和使用,可以參考redis官網。

需要考慮的問題

1、用什么操作redis?幸虧redis已經提供了jedis客戶端用于java應用程序,直接調用jedis API即可。
2、怎么實現加鎖?“鎖”其實是一個抽象的概念,將這個抽象概念變為具體的東西,就是一個存儲在redis里的key-value對,key是于商品ID相關的字符串來唯一標識,value其實并不重要,因為只要這個唯一的key-value存在,就表示這個商品已經上鎖。
3、如何釋放鎖?既然key-value對存在就表示上鎖,那么釋放鎖就自然是在redis里刪除key-value對。
4、阻塞還是非阻塞?筆者采用了阻塞式的實現,若線程發現已經上鎖,會在特定時間內輪詢鎖。
5、如何處理異常情況?比如一個線程把一個商品上了鎖,但是由于各種原因,沒有完成操作(在上面的業務場景里就是沒有將庫存-1寫入數據庫),自然沒有釋放鎖,這個情況筆者加入了鎖超時機制,利用redis的expire命令為key設置超時時長,過了超時時間redis就會將這個key自動刪除,即強制釋放鎖(可以認為超時釋放鎖是一個異步操作,由redis完成,應用程序只需要根據系統特點設置超時時間即可)。

talk is cheap,show me the code

在代碼實現層面,注解有并發的方法和參數,通過動態代理獲取注解的方法和參數,在代理中加鎖,執行完被代理的方法后釋放鎖。

幾個注解定義:

cachelock是方法級的注解,用于注解會產生并發問題的方法:

@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface CacheLock { String lockedPrefix() default "";//redis 鎖key的前綴 long timeOut() default 2000;//輪詢鎖的時間 int expireTime() default 1000;//key在redis里存在的時間,1000S}

lockedObject是參數級的注解,用于注解商品ID等基本類型的參數:

@Target(ElementType.PARAMETER)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface LockedObject { //不需要值}

LockedComplexObject也是參數級的注解,用于注解自定義類型的參數:

@Target(ElementType.PARAMETER)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface LockedComplexObject { String field() default "";//含有成員變量的復雜對象中需要加鎖的成員變量,如一個商品對象的商品ID}

CacheLockInterceptor實現InvocationHandler接口,在invoke方法中獲取注解的方法和參數,在執行注解的方法前加鎖,執行被注解的方法后釋放鎖:

public class CacheLockInterceptor implements InvocationHandler{ public static int ERROR_COUNT = 0; private Object proxied; public CacheLockInterceptor(Object proxied) { this.proxied = proxied; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { CacheLock cacheLock = method.getAnnotation(CacheLock.class); //沒有cacheLock注解,pass if(null == cacheLock){  System.out.println("no cacheLock annotation");    return method.invoke(proxied, args); } //獲得方法中參數的注解 Annotation[][] annotations = method.getParameterAnnotations(); //根據獲取到的參數注解和參數列表獲得加鎖的參數 Object lockedObject = getLockedObject(annotations,args); String objectValue = lockedObject.toString(); //新建一個鎖 RedisLock lock = new RedisLock(cacheLock.lockedPrefix(), objectValue); //加鎖 boolean result = lock.lock(cacheLock.timeOut(), cacheLock.expireTime()); if(!result){//取鎖失敗  ERROR_COUNT += 1;  throw new CacheLockException("get lock fail"); } try{  //加鎖成功,執行方法  return method.invoke(proxied, args); }finally{  lock.unlock();//釋放鎖 } } /** *  * @param annotations * @param args * @return * @throws CacheLockException */ private Object getLockedObject(Annotation[][] annotations,Object[] args) throws CacheLockException{ if(null == args || args.length == 0){  throw new CacheLockException("方法參數為空,沒有被鎖定的對象"); } if(null == annotations || annotations.length == 0){  throw new CacheLockException("沒有被注解的參數"); } //不支持多個參數加鎖,只支持第一個注解為lockedObject或者lockedComplexObject的參數 int index = -1;//標記參數的位置指針 for(int i = 0;i < annotations.length;i++){  for(int j = 0;j < annotations[i].length;j++){  if(annotations[i][j] instanceof LockedComplexObject){//注解為LockedComplexObject   index = i;   try {   return args[i].getClass().getField(((LockedComplexObject)annotations[i][j]).field());   } catch (NoSuchFieldException | SecurityException e) {   throw new CacheLockException("注解對象中沒有該屬性" + ((LockedComplexObject)annotations[i][j]).field());   }  }  if(annotations[i][j] instanceof LockedObject){   index = i;   break;  }  }  //找到第一個后直接break,不支持多參數加鎖  if(index != -1){  break;  } } if(index == -1){  throw new CacheLockException("請指定被鎖定參數"); } return args[index]; }}

最關鍵的RedisLock類中的lock方法和unlock方法:

/** * 加鎖 * 使用方式為: * lock(); * try{ * executeMethod(); * }finally{ * unlock(); * } * @param timeout timeout的時間范圍內輪詢鎖 * @param expire 設置鎖超時時間 * @return 成功 or 失敗 */ public boolean lock(long timeout,int expire){ long nanoTime = System.nanoTime(); timeout *= MILLI_NANO_TIME; try {  //在timeout的時間范圍內不斷輪詢鎖  while (System.nanoTime() - nanoTime < timeout) {  //鎖不存在的話,設置鎖并設置鎖過期時間,即加鎖  if (this.redisClient.setnx(this.key, LOCKED) == 1) {   this.redisClient.expire(key, expire);//設置鎖過期時間是為了在沒有釋放   //鎖的情況下鎖過期后消失,不會造成永久阻塞   this.lock = true;   return this.lock;  }  System.out.println("出現鎖等待");  //短暫休眠,避免可能的活鎖  Thread.sleep(3, RANDOM.nextInt(30));  }  } catch (Exception e) {  throw new RuntimeException("locking error",e); } return false; } public void unlock() { try {  if(this.lock){  redisClient.delKey(key);//直接刪除  } } catch (Throwable e) { } }

上述的代碼是框架性的代碼,現在來講解如何使用上面的簡單框架來寫一個秒殺函數。

先定義一個接口,接口里定義了一個秒殺方法:

public interface SeckillInterface {/***現在暫時只支持在接口方法上注解*/ //cacheLock注解可能產生并發的方法 @CacheLock(lockedPrefix="TEST_PREFIX") public void secKill(String userID,@LockedObject Long commidityID);//最簡單的秒殺方法,參數是用戶ID和商品ID??赡苡卸鄠€線程爭搶一個商品,所以商品ID加上LockedObject注解}

上述SeckillInterface接口的實現類,即秒殺的具體實現:

public class SecKillImpl implements SeckillInterface{ static Map<Long, Long> inventory ; static{ inventory = new HashMap<>(); inventory.put(10000001L, 10000l); inventory.put(10000002L, 10000l); } @Override public void secKill(String arg1, Long arg2) { //最簡單的秒殺,這里僅作為demo示例 reduceInventory(arg2); } //模擬秒殺操作,姑且認為一個秒殺就是將庫存減一,實際情景要復雜的多 public Long reduceInventory(Long commodityId){ inventory.put(commodityId,inventory.get(commodityId) - 1); return inventory.get(commodityId); }}

模擬秒殺場景,1000個線程來爭搶兩個商品:

@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++){  threads[i] = new Thread(new Runnable() {  public void run() {   try {   //等待在一個信號量上,掛起   beginCount.await();   //用動態代理的方式調用secKill方法   SeckillInterface proxy = (SeckillInterface) Proxy.newProxyInstance(SeckillInterface.class.getClassLoader(),     new Class[]{SeckillInterface.class}, new CacheLockInterceptor(testClass));   proxy.secKill("test", commidityId1);   endCount.countDown();   } catch (InterruptedException e) {   // TODO Auto-generated catch block   e.printStackTrace();   }  }  });  threads[i].start(); } //再起500個線程,秒殺第二件商品 for(int i= splitPoint;i < threadCount;i++){  threads[i] = new Thread(new Runnable() {  public void run() {   try {   //等待在一個信號量上,掛起   beginCount.await();   //用動態代理的方式調用secKill方法   SeckillInterface proxy = (SeckillInterface) Proxy.newProxyInstance(SeckillInterface.class.getClassLoader(),     new Class[]{SeckillInterface.class}, new CacheLockInterceptor(testClass));   proxy.secKill("test", commidityId2);   //testClass.testFunc("test", 10000001L);   endCount.countDown();   } catch (InterruptedException e) {   // TODO Auto-generated catch block   e.printStackTrace();   }  }  });  threads[i].start(); } long startTime = System.currentTimeMillis(); //主線程釋放開始信號量,并等待結束信號量,這樣做保證1000個線程做到完全同時執行,保證測試的正確性 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(); } }

在正確的預想下,應該每個商品的庫存都減少了500,在多次試驗后,實際情況符合預想。如果不采用鎖機制,會出現庫存減少499,498的情況。
這里采用了動態代理的方法,利用注解和反射機制得到分布式鎖ID,進行加鎖和釋放鎖操作。當然也可以直接在方法進行這些操作,采用動態代理也是為了能夠將鎖操作代碼集中在代理中,便于維護。
通常秒殺場景發生在web項目中,可以考慮利用spring的AOP特性將鎖操作代碼置于切面中,當然AOP本質上也是動態代理。

小結

這篇文章從業務場景出發,從抽象到實現闡述了如何利用redis實現分布式鎖,完成簡單的秒殺功能,也記錄了筆者思考的過程,希望能給閱讀到本篇文章的人一些啟發。

源碼倉庫

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持VEVB武林網。


注:相關教程知識閱讀請移步到Redis頻道。
發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb
欧美黑人视频一区| 97国产一区二区精品久久呦| 亚洲毛片在线观看.| 国产精品亚洲网站| 18一19gay欧美视频网站| 亚洲色图偷窥自拍| 亚洲视频电影图片偷拍一区| 国产日韩在线播放| 色妞在线综合亚洲欧美| 欧美激情视频在线观看| 欧洲永久精品大片ww免费漫画| 中文字幕亚洲一区二区三区五十路| 人人爽久久涩噜噜噜网站| 7m第一福利500精品视频| 国产精品黄视频| www.日韩免费| 北条麻妃一区二区三区中文字幕| 日韩av免费在线看| 亚洲视频综合网| 国产精品欧美日韩| 81精品国产乱码久久久久久| 日韩成人在线电影网| 亚洲人成免费电影| 琪琪第一精品导航| 欧美极度另类性三渗透| 国产亚洲欧美aaaa| 在线视频一区二区| 91高清免费视频| 91视频九色网站| 亚洲三级免费看| 国产精品96久久久久久又黄又硬| 欧美有码在线视频| 91网在线免费观看| 亚洲欧美制服中文字幕| 成人福利视频在线观看| 日韩电影在线观看中文字幕| 久久夜色精品国产亚洲aⅴ| 欧美在线性爱视频| 日韩中文字幕av| 国内精品久久久久久久久| 成人亚洲综合色就1024| 日韩精品免费在线播放| 亚洲a在线观看| 国产精品三级久久久久久电影| 成人欧美一区二区三区黑人孕妇| 欧美精品videos性欧美| 国产日韩在线亚洲字幕中文| 国产成人一区二区三区电影| 国产精品9999| 国产精品免费小视频| 日韩欧美国产高清91| 秋霞午夜一区二区| 欧美日韩国产精品一区二区不卡中文| 亚洲裸体xxxx| 国内自拍欧美激情| 亚洲视频欧美视频| 精品女同一区二区三区在线播放| 亚洲片国产一区一级在线观看| 亚洲美女视频网| 51ⅴ精品国产91久久久久久| 国产亚洲欧美日韩精品| 亚洲精品狠狠操| 欧美日韩一区二区在线| 亚洲va欧美va国产综合剧情| 这里只有精品在线观看| 91九色蝌蚪国产| 尤物九九久久国产精品的分类| 色777狠狠综合秋免鲁丝| 精品亚洲一区二区三区四区五区| 日韩欧美视频一区二区三区| 中文字幕日韩欧美在线视频| 欧美成人h版在线观看| 国产精品v片在线观看不卡| 国产成人啪精品视频免费网| 日韩中文字幕网址| 国产视频精品自拍| 久久久久久这里只有精品| 色中色综合影院手机版在线观看| 国产亚洲欧美日韩美女| 2025国产精品视频| 亚洲人成在线观看| 中文字幕亚洲欧美日韩高清| 亚洲va国产va天堂va久久| 久久久久这里只有精品| 插插插亚洲综合网| 久久久中精品2020中文| 日韩精品中文字幕视频在线| 国产精品一区=区| 精品福利在线观看| 久久99精品久久久久久噜噜| 久久久亚洲国产| 国产精品视频区1| 91成人国产在线观看| 国模视频一区二区三区| 最近2019中文字幕一页二页| 欧美成人性色生活仑片| 欧美电影免费播放| 国产日韩欧美夫妻视频在线观看| 日韩电影中文字幕在线观看| 欧美另类暴力丝袜| 色爱av美腿丝袜综合粉嫩av| 91性高湖久久久久久久久_久久99| 亚洲无限乱码一二三四麻| 成人av在线网址| 精品女同一区二区三区在线播放| 亚洲va久久久噜噜噜久久天堂| 日韩hd视频在线观看| 国产精品丝袜一区二区三区| 日韩欧美在线视频观看| 日韩视频欧美视频| 亚洲精品国产suv| 亚洲第一中文字幕在线观看| 亚洲视频一区二区| 亚洲精品久久久久中文字幕二区| 久久精品91久久久久久再现| 亚洲视频在线视频| 久久精品国产久精国产思思| 成人有码在线视频| 国产成人啪精品视频免费网| 精品国产一区二区三区久久久| 色老头一区二区三区在线观看| 欧美成人精品三级在线观看| 中文字幕国产亚洲2019| 成人免费大片黄在线播放| 成人羞羞国产免费| 亚洲国产成人爱av在线播放| 日韩最新av在线| 中文字幕在线观看日韩| 欧美午夜女人视频在线| 九九热精品视频国产| 国产精品一区久久| 久久777国产线看观看精品| 1769国产精品| 韩国三级电影久久久久久| 日韩精品在线观看视频| 国产美女扒开尿口久久久| 久久久久日韩精品久久久男男| 91久久久久久国产精品| 亚洲区中文字幕| 91免费国产网站| 欧美激情精品久久久久久| www.日韩视频| 国产精品久久久久免费a∨大胸| 日韩美女av在线| 青青青国产精品一区二区| 91精品久久久久久久久久久久久| 国产日韩在线观看av| 欧美国产一区二区三区| 91色精品视频在线| 亚洲第一av网| 永久免费毛片在线播放不卡| 国语自产精品视频在线看抢先版图片| 国产成人综合一区二区三区| 欧美在线欧美在线| 国产99在线|中文| 日韩资源在线观看| 亚洲片在线观看| 亚洲国产精品女人久久久| 亚洲国产精彩中文乱码av在线播放| 中文字幕不卡av| 一本色道久久综合狠狠躁篇的优点| 91在线免费观看网站| 国产亚洲精品久久久久动| 亚洲黄色免费三级|