1. 程式人生 > >SpringMVC+Redis實現分散式鎖實現秒殺功能

SpringMVC+Redis實現分散式鎖實現秒殺功能

1.實現分散式鎖的幾種方案
1.Redis實現 (推薦)
2.Zookeeper實現
3.資料庫實現
Redis實現分散式鎖
*
* 在叢集等多伺服器中經常使用到同步處理一下業務,這是普通的事務是滿足不了業務需求,需要分散式鎖
*
* 分散式鎖的常用3種實現:
* 0.資料庫樂觀鎖實現
* 1.Redis實現 — 使用redis的setnx()、get()、getset()方法,用於分散式鎖,解決死鎖問題
* 2.Zookeeper實現
* 參考:

http://surlymo.iteye.com/blog/2082684
* http://www.jb51.net/article/103617.htm
* http://www.hollischuang.com/archives/1716?utm_source=tuicool&utm_medium=referral
* 1、實現原理:
基於zookeeper瞬時有序節點實現的分散式鎖,其主要邏輯如下(該圖來自於IBM網站)。大致思想即為:每個客戶端對某個功能加鎖時,在zookeeper上的與該功能對應的指定節點的目錄下,生成一個唯一的瞬時有序節點。判斷是否獲取鎖的方式很簡單,只需要判斷有序節點中序號最小的一個。當釋放鎖的時候,只需將這個瞬時節點刪除即可。同時,其可以避免服務宕機導致的鎖無法釋放,而產生的死鎖問題。
2、優點
鎖安全性高,zk可持久化
3、缺點
效能開銷比較高。因為其需要動態產生、銷燬瞬時節點來實現鎖功能。
4、實現
可以直接採用zookeeper第三方庫curator即可方便地實現分散式鎖
*
* Redis實現分散式鎖的原理:
* 1.通過setnx(lock_timeout)實現,如果設定了鎖返回1, 已經有值沒有設定成功返回0
* 2.死鎖問題:通過實踐來判斷是否過期,如果已經過期,獲取到過期時間get(lockKey),然後getset(lock_timeout)判斷是否和get相同,
* 相同則證明已經加鎖成功,因為可能導致多執行緒同時執行getset(lock_timeout)方法,這可能導致多執行緒都只需getset後,對於判斷加鎖成功的執行緒,
* 再加expire(lockKey, LOCK_TIMEOUT, TimeUnit.MILLISECONDS)過期時間,防止多個執行緒同時疊加時間,導致鎖時效時間翻倍
* 3.針對叢集伺服器時間不一致問題,可以呼叫redis的time()獲取當前時間
2.Redis分分散式鎖的程式碼實現
1.定義鎖介面

/**
 * @Description: Redis分散式鎖介面
 * @Author: fxb
 * @CreateDate: 2018/3/29 15:43
 * @Version: 1.0
 */
public interface RedisLockService {
    /**
     * 加鎖成功,返回加鎖時間
     * @param lockKey
     * @param threadName
     * @return
     */
    public Long  lock(String lockKey);

    /**
     * 解鎖, 需要更新加鎖時間,判斷是否有許可權
     * @param
lockKey * @param lockValue * @param threadName */
public void unlock(String lockKey, long lockValue); /** * 多伺服器叢集,使用下面的方法,代替System.currentTimeMillis(),獲取redis時間,避免多服務的時間不一致問題!!! * @return */ public long currtTimeForRedis(); }

2、實現鎖實現

package com.jason.mrht.service.websrv.impl;

import com.jason.mrht.service.websrv.RedisLockService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

/**
 * @author fxb
 * @date 2018/3/29 15:43
 */
@Service
public class RedisLockServiceImpl extends BaseWebSrvService implements RedisLockService {

    /**
     * 加鎖超時時間,單位毫秒, 即:加鎖時間內執行完操作,如果未完成會有並發現象
     */
    private static final long LOCK_TIMEOUT = 5 * 1000;

    private static final Logger LOG = LoggerFactory.getLogger(RedisLockServiceImpl.class);

    /**
     * 取到鎖加鎖 取不到鎖一直等待直到獲得鎖
     */
    @Override
    public Long lock(final String lockKey) {
        LOG.info("開始執行加鎖");
        //迴圈獲取鎖
        while (true) {
            //鎖時間
            final Long lock_timeout = System.currentTimeMillis() + LOCK_TIMEOUT + 1;
            if (otherCache.execute(new RedisCallback<Boolean>() {
                @Override
                public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                    JdkSerializationRedisSerializer jdkSerializer = new JdkSerializationRedisSerializer();
                    byte[] value = jdkSerializer.serialize(lock_timeout);
                    return connection.setNX(lockKey.getBytes(), value);
                }
            })) {
                //如果加鎖成功
                LOG.info("加鎖成功++++++++111111111");
                //設定超時時間,釋放記憶體
                otherCache.expire(lockKey, LOCK_TIMEOUT, TimeUnit.MILLISECONDS);
                return lock_timeout;
            } else {
                // redis裡的時間
                Long currt_lock_timeout_Str = (Long) otherCache.opsForValue().get(lockKey);
                //鎖已經失效
                if (currt_lock_timeout_Str != null && currt_lock_timeout_Str < System.currentTimeMillis()) {
                    // 判斷是否為空,不為空的情況下,說明已經失效,如果被其他執行緒設定了值,則第二個條件判斷是無法執行
                    Long old_lock_timeout_Str = (Long) otherCache.opsForValue().getAndSet(lockKey, lock_timeout);
                    // 獲取上一個鎖到期時間,並設定現在的鎖到期時間
                    if (old_lock_timeout_Str != null && old_lock_timeout_Str.equals(currt_lock_timeout_Str)) {
                        // 如過這個時候,多個執行緒恰好都到了這裡,但是隻有一個執行緒的設定值和當前值相同,他才有權利獲取鎖
                        LOG.info("加鎖成功+++++++2222222222");
                        //設定超時時間,釋放記憶體
                        otherCache.expire(lockKey, LOCK_TIMEOUT, TimeUnit.MILLISECONDS);
                        //返回加鎖時間
                        return lock_timeout;
                    }
                }
            }
            try {
                LOG.info("等待加鎖,睡眠100毫秒");
                //睡眠100毫秒
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void unlock(String lockKey, long lockvalue) {
        //正常直接刪除 如果異常關閉判斷加鎖會判斷過期時間
        LOG.info("執行解鎖==========");
        // redis裡的時間
        Long currt_lock_timeout_Str = (Long) otherCache.opsForValue().get(lockKey);
        //如果是加鎖者 則刪除鎖 如果不是則等待自動過期 重新競爭加鎖
        if (currt_lock_timeout_Str != null && currt_lock_timeout_Str == lockvalue) {
            //刪除鍵
            otherCache.delete(lockKey);
            LOG.info("解鎖成功-----------------");
        }
    }

    /**
     * 多伺服器叢集,使用下面的方法,代替System.currentTimeMillis(),獲取redis時間,避免多服務的時間不一致問題!!!
     *
     * @return
     */
    @Override
    public long currtTimeForRedis() {
        return otherCache.execute(new RedisCallback<Long>() {
            @Override
            public Long doInRedis(RedisConnection redisConnection) throws DataAccessException {
                return redisConnection.time();
            }
        });
    }

}

3.分散式鎖驗證

package com.jason.mrht.core.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * 測試模組
 * @author bao
 */
@Controller
@RequestMapping("/api/redis")
public class TestController extends BaseController {

    /**
     * 秒殺商品數量10個
     */
    private int goodNum = 10;

    private static final String LOCK_NO = "redis_distribution_lock_no_";

    private static int i = 0;

    private int taskNum = 1000;

    /**
     * redis分散式鎖測試
     * 模擬1000個執行緒同時執行業務,修改資源
     */
    @ResponseBody
    @RequestMapping(value = "/redisLock", method = RequestMethod.GET)
    public String testRedisDistributionLock1() {
        /** task(); */
        for (int i = 0; i < taskNum; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    task();
                }
            }).start();
        }
        return "OK";
    }

    /**
     * 建立一個redis分散式鎖任務
     */
    private void task() {
        //加鎖時間
        Long lockTime;
        if ((lockTime = serviceTemplate.getRedisLockService().lock((LOCK_NO + 1) + "")) != null) {
            //開始執行任務
            logger.info("當前庫存的數量:"+goodNum);
            if (goodNum == 0) {
                logger.info("停止減庫存任務");
            } else {
                goodNum--;
                logger.info("開始減庫存任務");
            }
            logger.info("任務執行中" + (i++));
            // 任務執行完畢 關閉鎖
            serviceTemplate.getRedisLockService().unlock((LOCK_NO + 1) + "", lockTime);
        }
    }

}

4.結果驗證:

  在Controller中模擬了1000個執行緒,通過執行緒池方式提交,每次20個執行緒搶佔分散式鎖,搶到分散式鎖的執行程式碼,沒搶到的等待
  模擬秒殺10個商品1000個人來爭奪

結果
5、模擬使用者的併發請求

package com.jason.mrht.common.utils;

/**
 * @Description: 類作用描述
 * @Author: fxb
 * @CreateDate: 2018/3/29 18:55
 * @Version: 1.0
 */

import com.jason.mrht.common.exception.HttpException;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CountDownLatch;


/**
 * 模擬使用者的併發請求,檢測使用者樂觀鎖的效能問題
 *
 * @author zzg
 * @date 2017-02-10
 */
public class ConcurrentTest {

    final static SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args){
        //模擬10000人併發請求,使用者錢包
        CountDownLatch latch=new CountDownLatch(1);
        //模擬10000個使用者
        for(int i=0;i<10000;i++){
            AnalogUser analogUser = new AnalogUser("user"+i,"58899dcd-46b0-4b16-82df-bdfd0d953bfb"+i,"1","20.024",latch);
            analogUser.start();
        }
        //計數器減一  所有執行緒釋放 併發訪問。
        latch.countDown();
        System.out.println("所有模擬請求結束  at "+sdf.format(new Date()));

    }

    static class AnalogUser extends Thread{
        //模擬使用者姓名
        String workerName;
        String openId;
        String openType;
        String amount;
        CountDownLatch latch;

        public AnalogUser(String workerName, String openId, String openType, String amount,
                          CountDownLatch latch) {
            super();
            this.workerName = workerName;
            this.openId = openId;
            this.openType = openType;
            this.amount = amount;
            this.latch = latch;
        }

        @Override
        public void run() {
            // TODO Auto-generated method stub
            try {
                latch.await(); //一直阻塞當前執行緒,直到計時器的值為0
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            post();//傳送post 請求


        }

        public void post(){
            String result = "";
            System.out.println("模擬使用者: "+workerName+" 開始傳送模擬請求  at "+sdf.format(new Date()));
            try {


            result = HttpUtil.sendGet("http://localhost:8080/api/collect/distribution/redis/lock1",null);
            }catch (HttpException e){

            }
                    //sendPost("http://localhost:8080/Settlement/wallet/walleroptimisticlock.action", "openId="+openId+"&openType="+openType+"&amount="+amount);
            System.out.println("操作結果:"+result);
            System.out.println("模擬使用者: "+workerName+" 模擬請求結束  at "+sdf.format(new Date()));

        }
    }

}