項目總結64:分別使用Redisson和Zookeeper分布式鎖模擬模擬搶紅包業務


項目總結64:分別使用Redisson和Zookeeper分布式鎖模擬模擬搶紅包業務

業務場景

  模擬1000人在10秒內搶10000(或1000)元紅包,金額在1-100不等;

使用的框架或軟件:

  框架或組件:Springboot(基礎框架)、Redisson(實現分布式鎖)、Zookeeper(實現分布式鎖方案)、Ngnix(負載均衡),Redis(紅包數據存取數據庫)

  系統或軟件:Linux服務器、Jmeter(模擬並發請求)

 

具體代碼示例和測試結果(公用方法放在文中附錄)

情況1- 單機服務——沒有任何線程安全考慮——出現數據錯誤

@GetMapping("/get/money")
public String getRedPackage(){

    Map map = new HashMap();
    Object o = redisTemplate.opsForValue().get(KEY_RED_PACKAGE_MONEY);
    int remainMoney = Integer.parseInt(String.valueOf(o));
    if(remainMoney <= 0 ){
        map.put("result","紅包已搶完");
        return ReturnModel.success(map).appendToString();
    }
    int randomMoney = (int) (Math.random() * 100);
    if(randomMoney > remainMoney){
        randomMoney = remainMoney;
    }
    int newRemainMoney = remainMoney-randomMoney;
    redisTemplate.opsForValue().set(KEY_RED_PACKAGE_MONEY,newRemainMoney);
    String result = "原有金額:" + remainMoney + " 紅包金額:" + randomMoney + "剩余金額:" + newRemainMoney;
    System.out.println(result);
    map.put("result",result);

    return ReturnModel.success(map).appendToString();
}

原有金額:1000 紅包金額:49剩余金額:951 原有金額:1000 紅包金額:62剩余金額:938
原有金額:1000 紅包金額:61剩余金額:939
原有金額:1000 紅包金額:93剩余金額:907
原有金額:1000 紅包金額:73剩余金額:927
原有金額:939 紅包金額:65剩余金額:874
原有金額:939 紅包金額:16剩余金額:923
原有金額:939 紅包金額:30剩余金額:909

 

情況2- 單台服務——使用Lock鎖——數據正常;Lock在單服務器是線程安全的

public static Lock lock = new ReentrantLock(); 
@GetMapping("/get/money/lock")
public String getRedPackageLock(){
    Map map = new HashMap();
    lock.lock(); try{
        Object o = redisTemplate.opsForValue().get(KEY_RED_PACKAGE_MONEY);
        int remainMoney = Integer.parseInt(String.valueOf(o));
        if(remainMoney <= 0 ){
            map.put("result","紅包已搶完");
            return ReturnModel.success(map).appendToString();
        }
        int randomMoney = (int) (Math.random() * 100);
        if(randomMoney > remainMoney){
            randomMoney = remainMoney;
        }
        int newRemainMoney = remainMoney-randomMoney;
        redisTemplate.opsForValue().set(KEY_RED_PACKAGE_MONEY,newRemainMoney);
        String result = "原有金額:" + remainMoney + " 紅包金額:" + randomMoney + "剩余金額:" + newRemainMoney;
        System.out.println(result);
        map.put("result",result);
        return ReturnModel.success(map).appendToString();
    }finally {
        lock.unlock();
    }
}

原有金額:1000 紅包金額:11剩余金額:989
原有金額:989 紅包金額:48剩余金額:941
原有金額:941 紅包金額:17剩余金額:924
原有金額:924 紅包金額:89剩余金額:835
原有金額:835 紅包金額:63剩余金額:772
原有金額:772 紅包金額:77剩余金額:695
原有金額:695 紅包金額:76剩余金額:619
原有金額:619 紅包金額:8剩余金額:611
原有金額:611 紅包金額:67剩余金額:544
原有金額:544 紅包金額:9剩余金額:535
原有金額:535 紅包金額:78剩余金額:457
......

 

情況3- 兩台服務器——使用Lock鎖——數據異常(代碼情況2一樣);Lock在鍍鈦服務器下是非線程安全的

負載均衡配置

  使用Nginx配置負載均衡,Ngnix安裝參考博客;配置參考博客;部署兩個服務分別是8001和8002端口,Nginx暴露8080端口,轉發請求到8001和8002;

 

 

 nginx配置

http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;
    ##定義負載均衡真實服務器IP:端口號 weight表示權重
    upstream myserver{
        server   XX.XX.XX.XX:8001 weight=1;
        server   XX.XX.XX.XX:8002 weight=1;
     }
    server {
        listen   8080;
        location  / {
            proxy_pass   http://myserver;
            proxy_connect_timeout 10;
        }
    }  


}

 

情況3-1- 兩台服務器——使用Redisson分布式鎖——數據正常

        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.31.Final</version>
        </dependency>
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.6.5</version>
        </dependency>

 

@Configuration
public class RedissonConfig {

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private String port;

    @Value("${spring.redis.password}")
    private String password;


    @Bean
    public RedissonClient getRedisson(){

        Config config = new Config();
        config.useSingleServer().setAddress("redis://" + host + ":" + port).setPassword(password);
        return Redisson.create(config);
    }

}

 

 

@Autowired private RedissonClient redissonClient; //3-搶紅包-redisson
@GetMapping("/get/money/redisson")
public String getRedPackageRedison(){
 RLock rLock = redissonClient.getLock("secKill"); rLock.lock();
    Map map = new HashMap();
    try{
        Object o = redisTemplate.opsForValue().get(KEY_RED_PACKAGE_MONEY);
        int remainMoney = Integer.parseInt(String.valueOf(o));
        if(remainMoney <= 0 ){
            map.put("result","紅包已搶完");
            return ReturnModel.success(map).appendToString();
        }
        int randomMoney = (int) (Math.random() * 100);
        if(randomMoney > remainMoney){
            randomMoney = remainMoney;
        }
        int newRemainMoney = remainMoney-randomMoney;
        redisTemplate.opsForValue().set(KEY_RED_PACKAGE_MONEY,newRemainMoney);
        String result = "原有金額:" + remainMoney + " 紅包金額:" + randomMoney + "剩余金額:" + newRemainMoney;
        System.out.println(result);
        map.put("result",result);
        return ReturnModel.success(map).appendToString();
    }finally {
      rLock.unlock();
    }
}

 

情況3-2- 兩台服務器——使用Zookeeper分布式鎖——數據正常

 

        <!-- ZooKeeper 之 Curator-->
        <!-- ZooKeeper版本號為4的話,機器安裝zookeeper的版本要求是3.5及其以上的版本-->
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>4.0.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>4.0.1</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.curator/curator-client -->
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-client</artifactId>
            <version>4.0.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>3.5.4-beta</version>
        </dependency>
        <dependency>
            <groupId>com.google.collections</groupId>
            <artifactId>google-collections</artifactId>
            <version>1.0</version>
        </dependency>

 

 

@Configuration
public class ZkConfiguration {
    /**
     * 重試次數
     */
    @Value("${curator.retryCount}")
    private int retryCount;
    /**
     * 重試間隔時間
     */
    @Value("${curator.elapsedTimeMs}")
    private int elapsedTimeMs;
    /**
     * 連接地址
     */
    @Value("${curator.connectString}")
    private String connectString;
    /**
     * Session過期時間
     */
    @Value("${curator.sessionTimeoutMs}")
    private int sessionTimeoutMs;
    /**
     * 連接超時時間
     */
    @Value("${curator.connectionTimeoutMs}")
    private int connectionTimeoutMs;

    @Bean(initMethod = "start")
    public CuratorFramework curatorFramework() {
        return CuratorFrameworkFactory.newClient(
                connectString,
                sessionTimeoutMs,
                connectionTimeoutMs,
                new RetryNTimes(retryCount,elapsedTimeMs));
    }

    /**
     * Distributed lock by zookeeper distributed lock by zookeeper.
     *
     * @return the distributed lock by zookeeper
     */
    @Bean(initMethod = "init")
    public DistributedLockByZookeeper distributedLockByZookeeper() {
        return new DistributedLockByZookeeper();
    }
}

 

 

@Slf4j
public class DistributedLockByZookeeper {
    private final static String ROOT_PATH_LOCK = "myk";

    private CountDownLatch countDownLatch = new CountDownLatch(1);

    /**
     * The Curator framework.
     */
    @Autowired
    CuratorFramework curatorFramework;

    /**
     * 獲取分布式鎖
     * 創建一個臨時節點,
     *
     * @param path the path
     */
    public void acquireDistributedLock(String path) {
        String keyPath = "/" + ROOT_PATH_LOCK + "/" + path;
        while (true) {
            try {
                curatorFramework.create()
                        .creatingParentsIfNeeded()
                        .withMode(CreateMode.EPHEMERAL)
                        .withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
                        .forPath(keyPath);
                //log.info("success to acquire lock for path:{}", keyPath);
                break;
            } catch (Exception e) {
                //搶不到鎖,進入此處!
                //log.info("failed to acquire lock for path:{}", keyPath);
                //log.info("while try again .......");
                try {
                    if (countDownLatch.getCount() <= 0) {
                        countDownLatch = new CountDownLatch(1);
                    }
                    //避免請求獲取不到鎖,重復的while,浪費CPU資源
                    countDownLatch.await();
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }
            }
        }
    }

    /**
     * 釋放分布式鎖
     *
     * @param path the  節點路徑
     * @return the boolean
     */
    public boolean releaseDistributedLock(String path) {
        try {
            String keyPath = "/" + ROOT_PATH_LOCK + "/" + path;
            if (curatorFramework.checkExists().forPath(keyPath) != null) {
                curatorFramework.delete().forPath(keyPath);
            }
        } catch (Exception e) {
            //log.error("failed to release lock,{}", e);
            return false;
        }
        return true;
    }

    /**
     * 創建 watcher 事件
     */
    private void addWatcher(String path) {
        String keyPath;
        if (path.equals(ROOT_PATH_LOCK)) {
            keyPath = "/" + path;
        } else {
            keyPath = "/" + ROOT_PATH_LOCK + "/" + path;
        }
        try {
            final PathChildrenCache cache = new PathChildrenCache(curatorFramework, keyPath, false);
            cache.start(PathChildrenCache.StartMode.POST_INITIALIZED_EVENT);
            cache.getListenable().addListener((client, event) -> {
                if (event.getType().equals(PathChildrenCacheEvent.Type.CHILD_REMOVED)) {
                    String oldPath = event.getData().getPath();
                    //log.info("上一個節點 " + oldPath + " 已經被斷開");
                    if (oldPath.contains(path)) {
                        //釋放計數器,讓當前的請求獲取鎖
                        countDownLatch.countDown();
                    }
                }
            });
        } catch (Exception e) {
            log.info("監聽是否鎖失敗!{}", e);
        }
    }

    /**
     * 創建父節點,並創建永久節點
     */
    public void init() {
        curatorFramework = curatorFramework.usingNamespace("lock-namespace");
        String path = "/" + ROOT_PATH_LOCK;
        try {
            if (curatorFramework.checkExists().forPath(path) == null) {
                curatorFramework.create()
                        .creatingParentsIfNeeded()
                        .withMode(CreateMode.PERSISTENT)
                        .withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
                        .forPath(path);
            }
            addWatcher(ROOT_PATH_LOCK);
            log.info("root path 的 watcher 事件創建成功");
        } catch (Exception e) {
            log.error("connect zookeeper fail,please check the log >> {}", e.getMessage(), e);
        }
    }

}

 

 

    @Autowired
    DistributedLockByZookeeper distributedLockByZookeeper;
    private final static String PATH = "red_package";
    //4-搶紅包-zookeeper
    @GetMapping("/get/money/zookeeper")
    public String getRedPackageZookeeper(){

        Boolean flag = false;
        distributedLockByZookeeper.acquireDistributedLock(PATH);
        Map map = new HashMap();
        try {
            Object o = redisTemplate.opsForValue().get(KEY_RED_PACKAGE_MONEY);
            int remainMoney = Integer.parseInt(String.valueOf(o));
            if(remainMoney <= 0 ){
                map.put("result","紅包已搶完");
                return ReturnModel.success(map).appendToString();
            }
            int randomMoney = (int) (Math.random() * 100);
            if(randomMoney > remainMoney){
                randomMoney = remainMoney;
            }
            int newRemainMoney = remainMoney-randomMoney;
            redisTemplate.opsForValue().set(KEY_RED_PACKAGE_MONEY,newRemainMoney);
            String result = "原有金額:" + remainMoney + " 紅包金額:" + randomMoney + "剩余金額:" + newRemainMoney;
            System.out.println(result);
            map.put("result",result);
            return ReturnModel.success(map).appendToString();
        } catch(Exception e){
            e.printStackTrace();
            flag = distributedLockByZookeeper.releaseDistributedLock(PATH);
            //System.out.println("releaseDistributedLock: " + flag);
            map.put("result","getRedPackageZookeeper catch exceeption");
            return ReturnModel.success(map).appendToString();
        }finally {
            flag = distributedLockByZookeeper.releaseDistributedLock(PATH);
            //System.out.println("releaseDistributedLock: " + flag);
        }
    }

 

 

附錄

1- 其他配置和類

application.properties文件

server.port=80

#配置redis
spring.redis.host=XX.XX.XX.XX
spring.redis.port=6379
spring.redis.password=xuegaotest1234
spring.redis.database=0
#重試次數
curator.retryCount=5
#重試間隔時間
curator.elapsedTimeMs=5000
# zookeeper 地址
curator.connectString=XX.XX.XX.XX:2181
# session超時時間
curator.sessionTimeoutMs=60000
# 連接超時時間
curator.connectionTimeoutMs=5000

 

ReturnModel 類

public class ReturnModel implements Serializable{


    private int code;
    private String msg;
    private Object data;




    public static ReturnModel success(Object obj){
        return new ReturnModel(200,"success",obj);
    }

    public String appendToString(){
        return  JSON.toJSONString(this);
    }

    public ReturnModel() {
    }

    public ReturnModel(int code, String msg, Object data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
}

 SeckillController類

    public final static String KEY_RED_PACKAGE_MONEY  = "key_red_package_money";


    @Autowired
    private RedisTemplate redisTemplate;

    //1-設置紅包
    @GetMapping("/set/money/{amount}")
    public String setRedPackage(@PathVariable Integer amount){
        redisTemplate.opsForValue().set(KEY_RED_PACKAGE_MONEY,amount);
        Object o = redisTemplate.opsForValue().get(KEY_RED_PACKAGE_MONEY);
        Map map = new HashMap();
        map.put("moneyTotal",Integer.parseInt(String.valueOf(o)));
        return ReturnModel.success(map).appendToString();
    }

 

 

流程解析

  1- Zookeeper分布鎖

1- 在ZkConfiguration類中加載CuratorFramework時,設置參數,實例化一個CuratorFramework類; 實例化過程中,執行CuratorFrameworkImpl類中的的start(),其中CuratorFrameworkImpl類是CuratorFramework的實現類;根據具體的細節可以參考博客

@Bean(initMethod = "start")
public CuratorFramework curatorFramework() {
    return CuratorFrameworkFactory.newClient( connectString,  sessionTimeoutMs, connectionTimeoutMs,  new RetryNTimes(retryCount,elapsedTimeMs));
}

 

2- 在ZkConfiguration類中加載DistributedLockByZookeeper時;執行其中的init()方法;init()方法中主要是創建父節點和添加監聽

/**
 * 創建父節點,並創建永久節點
 */
public void init() {
    curatorFramework = curatorFramework.usingNamespace("lock-namespace");
    String path = "/" + ROOT_PATH_LOCK;
    try {
        if (curatorFramework.checkExists().forPath(path) == null) {
            curatorFramework.create()
                    .creatingParentsIfNeeded()
                    .withMode(CreateMode.PERSISTENT)
                    .withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
                    .forPath(path);
        }
        addWatcher(ROOT_PATH_LOCK);
        log.info("root path 的 watcher 事件創建成功");
    } catch (Exception e) {
        log.error("connect zookeeper fail,please check the log >> {}", e.getMessage(), e);
    }
}

 

3- 在具體業務中調用distributedLockByZookeeper.acquireDistributedLock(PATH);獲取分布式鎖

/**
 * 獲取分布式鎖
 * 創建一個臨時節點,
 *
 * @param path the path
 */
public void acquireDistributedLock(String path) {
    String keyPath = "/" + ROOT_PATH_LOCK + "/" + path;
    while (true) {
        try {
            curatorFramework.create()
                    .creatingParentsIfNeeded()
                    .withMode(CreateMode.EPHEMERAL)
                    .withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
                    .forPath(keyPath);
            break;
        } catch (Exception e) {
            //搶不到鎖,進入此處!
            try {
                if (countDownLatch.getCount() <= 0) {
                    countDownLatch = new CountDownLatch(1);
                }
                //避免請求獲取不到鎖,重復的while,浪費CPU資源
                countDownLatch.await();
            } catch (InterruptedException e1) {
                e1.printStackTrace();
            }
        }
    }
}

 

4- 業務結束時調用distributedLockByZookeeper.releaseDistributedLock(PATH);釋放鎖

/**
 * 釋放分布式鎖
 *
 * @param path the  節點路徑
 * @return the boolean
 */
public boolean releaseDistributedLock(String path) {
    try {
        String keyPath = "/" + ROOT_PATH_LOCK + "/" + path;
        if (curatorFramework.checkExists().forPath(keyPath) != null) {
            curatorFramework.delete().forPath(keyPath);
        }
    } catch (Exception e) {
        return false;
    }
    return true;
}

 

 原理圖如下

 

 

 

 期間碰到的問題

問題: 項目啟動時:java.lang.ClassNotFoundException: com.google.common.base.Function

原因:缺少google-collections jar包;如下

<dependency>

    <groupId>com.google.collections</groupId>

    <artifactId>google-collections</artifactId>

    <version>1.0</version>

</dependency>

 

 

 

問題:項目啟動時:org.apache.curator.CuratorConnectionLossException: KeeperErrorCode = ConnectionLoss

原因:簡單說,就是連接失敗(可能原因的有很多);依次排查了zookeeper服務器防火牆、application.properties配置文件;最后發現IP的寫錯了,更正后就好了

 

問題:Jemter啟用多線程並發測試時:java.net.BindException: Address already in use: connect

原因和解決方案:參考博客

 

END

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM