redis分佈式鎖的實現

 在集羣系統中,有些資源的調用需要加鎖,由於服務是有多個資源對外提供的,資源之間的鎖使用JAVA的鎖是不可行的,所以需要使用其他的方式來實現加鎖,比如訂單與庫存的鎖。

   redis是基於內存的key-value的NOSQL數據庫,效率高,單機併發可達1K左右,其中setnx可以很方便實現併發鎖機制

下面是基於SpringBoot以及redis實現的分佈式鎖的工具類,可以在需要加鎖的地方加上註解即可靈活使用

/**
 * 該註解(@MethodLock)可以用在方法 上表示給發放加分佈式鎖
 * 與
 * @author Administrator
 *
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LockMethod {
    /**
     * 鎖的key
     * 本參數是必寫選項<br/>
     */
    String preKey() default "redisLock";
      /**
     * 持鎖時間,超時時間,持鎖超過此時間自動丟棄鎖<br/>
     * 單位毫秒,默認20秒<br/>
     * 如果爲0表示永遠不釋放鎖,知道過程執行完自動釋放鎖,
     * 在設置爲0的情況下toWait爲true是沒有意義的<br/>
     * 但是沒有比較強的業務要求下,不建議設置爲0
     */  
    int expireTime() default 20 ;
    /**
     * 當獲取鎖失敗,是繼續等待還是放棄<br/>
     * 默認爲繼續等待
     */  
    boolean toWait() default true;  
//    /**
//     * 沒有獲取到鎖的情況下且toWait()爲繼續等待,睡眠指定毫秒數繼續獲取鎖,也就是輪訓獲取鎖的時間<br/>
//     * 默認爲10毫秒
//     *  
//     * @return
//     */  
//    long sleepMills() default 10;  
    /**
     * 鎖獲取超時時間:<br/>
     * 沒有獲取到鎖的情況下且toWait()爲true繼續等待,最大等待時間,如果超時拋出
     * {@link java.util.concurrent.TimeoutException.TimeoutException}
     * ,可捕獲此異常做相應業務處理;<br/>
     * 單位毫秒,默認3秒鐘,如果設置爲0即爲沒有超時時間,一直獲取下去;
     *  
     * @return
     */  
    long maxSleepMills() default 3 * 1000;  

}





@Target({ ElementType.PARAMETER })  
@Retention(RetentionPolicy.RUNTIME)  
@Documented
public @interface LockParameter {
    /**
     * 含有成員變量的複雜對象中需要加鎖的成員變量,如一個商品對象的商品ID
     * 也就是複雜對象中的屬性名稱
     * 比如User user 即爲複雜對象  使用對象中的屬性 username來作爲sunKey
     * isObject=false 時候該功能才起作用
     * @return
     */
    String field() default "";
    /**
     * 是否是基本的簡單屬性  true表示是簡單屬性,false 表示是複雜對象
     * true:表示 int,long,String,char 等基本數據類型
     * false:表示對象模型 比如User
     * @return
     */
    boolean isSimple() default true;
}



@Aspect
@Component
public class RedisLockAop {
    
//    @Before("@annotation(CacheLock)")
//     public void requestLimit(JoinPoint point, CacheLock cacheLock)throws Throwable{
//        
//    }
    private static Logger logger = LoggerFactory.getLogger(RedisLockAop.class);
    @Autowired
    private SpringJedisUtilInterface redisUtil;
//    @Autowired
//    private RedisUtil redisUtil;
    /**
     * 通過前置攔截,攔截所有經過CacheLock註解過的方法
     * @param point
     * @param CacheLock
     * @throws Throwable
     */
    @Around("@annotation(LockMethod)")
    public Object requestLimit(ProceedingJoinPoint  joinPoint) throws Throwable{
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        LockMethod methodLock = method.getAnnotation(LockMethod.class);
        if(methodLock ==null){
             throw new RedisLockException("配置參數錯誤");  
        }
        Object[] args  = joinPoint.getArgs();
         // 得到被代理的方法
        //獲得方法中參數的註解
        Annotation[][] annotations = method.getParameterAnnotations();
        //根據獲取到的參數註解和參數列表獲得加鎖的參數
        String sunKey = getLockEndKey(annotations,args);
        String key = methodLock.preKey()+"_"+sunKey+"_lock";
        logger.info(key);
        // 超時時間  (最大的等待時間)
        long maxSleepMills =  methodLock.maxSleepMills();
//        獲得鎖之後最大可持有鎖的時間
        int expire = methodLock.expireTime();
//        是否等待
        boolean toWait = methodLock.toWait();
        boolean lock = false;
        Object obj = null;
        int radomInt = 0;// 每個線程隨機等待時間
        try {
//             說明沒有得到鎖,等待輪訓的去獲得鎖
             while (!lock) {
                 lock =getLock(key, expire);
                  // 得到鎖,沒有人加過相同的鎖  
                 if (lock) {
                     logger.info("線程獲得鎖開始執行"+"---"+redisUtil);
                     obj = joinPoint.proceed();  
                     break;  
                 }else if(toWait){
                     radomInt = new Random().nextInt(99);
                     maxSleepMills =maxSleepMills- radomInt;
                     logger.info("沒獲得鎖,隨機等待毫秒數:"+radomInt+"---"+redisUtil);
                     if(maxSleepMills<=0){
                         logger.info("獲取鎖資源等待超時"+"---"+redisUtil);
                         break;
//                         throw new CacheLockException("獲取鎖資源等待超時,等待超時");  
                     }
                     TimeUnit.MILLISECONDS.sleep(radomInt);  
                 }else{
                     break;  
                 }
             }
        } catch (Throwable e) {
            e.printStackTrace();
            logger.info(e.getMessage()+"--"+e);
            throw e;
        }finally{
            if(lock){
                logger.info("執行刪除操作");
                redisUtil.del(key );//直接刪除
            }
        }
        return obj;
    }

    
    
    
    /**
     *  從方法參數中找出@lockedComplexOnbject的參數,在redis中取該參數對應的鎖
     * @param annotations
     * @param args
     * @return
     * @throws RedisLockException
     */
    private String getLockEndKey(Annotation[][] annotations,Object[] args) throws RedisLockException{
        if(null == args || args.length == 0){
            throw new RedisLockException("方法參數爲空,沒有被鎖定的對象");
        }
        
        if(null == annotations || annotations.length == 0){
            throw new RedisLockException("沒有被註解的參數");
        }
         SortedMap<Integer, String> keys = new TreeMap<Integer, String>();
         String keyString;
        //直接在多個參數上註解
        for(int i = 0;i < annotations.length;i++){
            for(int j = 0;j < annotations[i].length;j++){
                if(annotations[i][j] instanceof Value){
                       Object arg = args[i];
                       logger.info("--Value args[i]--"+i+"------"+arg);
                }
                if(annotations[i][j] instanceof LockParameter){//註解爲LockedComplexObject
                    LockParameter lockParameter = (LockParameter)annotations[i][j];
                    Object arg = args[i];
                    logger.info("--lockParameter args[i]--"+i+"------"+arg);
                    try {
                        if(lockParameter.isSimple()){
                            keys.put(i, String.valueOf(arg));
                        }else{
                            keyString = args[i].getClass().getField(lockParameter.field()).toString();
                            keys.put(i,keyString);
                        }
                        
                    } catch (NoSuchFieldException |SecurityException e) {
                        e.printStackTrace();
                        throw new RedisLockException("註解對象中沒有該屬性"+lockParameter.field());
                    }
                }
            
            }
        }
        String sunKey ="";
        if (keys != null && keys.size() > 0) {  
              for (String key : keys.values()) {  
                  sunKey = sunKey + key;  
              }  
       }  
        return sunKey;
    }
    
     public boolean getLock(String key ,int expire) throws InterruptedException{
         if(redisUtil.setnx(key, String.valueOf(expire))==1){//1插入成功且key不存在,0未插入,key存在
             redisUtil.expired(key, expire);
             return true;
          }
         return false;
     }
}


測試用例


@Service
public class SecKillImpl implements SeckillInterface{
    public static Map<Long, Long> inventory ;
    static{
        inventory = new HashMap<>();
        inventory.put(10000001L, 10000l);
        inventory.put(10000002L, 10000l);
    }
    
    @Override
    @LockMethod(preKey="Seckill",expireTime=1)
    public void secKill(String arg1, @LockParameter Long arg2, int a) {
        //最簡單的秒殺,這裏僅作爲demo示例
        System.out.println("commodityId"+arg2+"  線程幾號="+a);
        reduceInventory(arg2);
    }
    //模擬秒殺操作,姑且認爲一個秒殺就是將庫存減一,實際情景要複雜的多
    public Long reduceInventory(Long commodityId){
        inventory.put(commodityId,inventory.get(commodityId) - 1);
        return inventory.get(commodityId);
    }



@RunWith(SpringRunner.class)
@SpringBootTest(classes = FirstSpringBootApplication.class,
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class SecKillTest {
    private static Long commidityId1 = 10000001L;
    private static Long commidityId2 = 10000002L;

     @Autowired
     private SeckillInterface proxy;
    
    @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++){
            int  a = i;
            threads[i] = new Thread(new  Runnable() {
                public void run() {
                    try {
                        //等待在一個信號量上,掛起
                        beginCount.await();
                        //用動態代理的方式調用secKill方法
                        proxy.secKill("test", commidityId1,a);
                        endCount.countDown();
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            });
            threads[i].start();

        }
        
        for(int i= splitPoint;i < threadCount;i++){
            int a =i;
            threads[i] = new Thread(new  Runnable() {
                public void run() {
                    try {
                        //等待在一個信號量上,掛起
                        beginCount.await();
                        //用動態代理的方式調用secKill方法
                        beginCount.await();
                        proxy.secKill("test", commidityId2,a );
                        //testClass.testFunc("test", 10000001L);
                        endCount.countDown();
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            });
            threads[i].start();

        }
        
        
        long startTime = System.currentTimeMillis();
        //主線程釋放開始信號量,並等待結束信號量
        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();
        }
    }
}


以下是項目下載地址:項目中包含了redis併發鎖,redis 的session共享  分佈式緩存  druid數據庫連接池,druid監控等

源碼下載地址:https://git.oschina.net/xufan0711/firstSpringboot/tree/master/  

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章