更新时间:2024-11-26 GMT+08:00

使用DCS实现热点资源顺序访问

方案概述

应用场景

在传统单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLock或synchronized)进行互斥控制。这种Java提供的原生锁机制可以保证在同一个Java虚拟机进程内的多个线程同步执行,避免出现无序现象。

但在互联网场景,例如在商品秒杀过程中,随着客户业务量上升,整个系统并发飙升,需要多台机器并发运行。例如当两个用户同时发起的请求分别落在两个不同的机器上时,虽然这两个请求可以同时执行,但是因为两个机器运行在两个不同的Java虚拟机中,因此每个机器加的锁不是同一个锁,而不同的锁只对属于自己Java虚拟机中的线程有效,对其他Java虚拟机的线程无效。此时,Java提供的原生锁机制在多机部署场景下就会失效,出现库存超卖的现象。

解决方案

基于上述场景,需要保证两台机器加的锁是同一个锁,用加锁的方式对某种资源进行顺序访问控制。这就需要分布式锁登场了。

分布式锁的思路是:在整个系统提供一个全局的、唯一的分配锁的“东西”,当每个系统需要加锁时,都向其获取一把锁,使不同的系统获取到的内容可以认为是同一把锁。

当前分布式加锁主要有三种方式:(磁盘)数据库、缓存数据库、Zookeeper。

使用DCS服务中Redis缓存实例实现分布式加锁,有几大优势:

  • 加锁操作简单,使用SET、GET、DEL等几条简单命令即可实现锁的获取和释放。
  • 性能优越,缓存数据的读写优于磁盘数据库与Zookeeper。
  • 可靠性强,DCS有主备和集群实例类型,避免单点故障。

对分布式应用加锁,能够避免出现库存超卖及无序访问等现象。本实践介绍如何使用Redis对分布式应用加锁。

前提条件

  • 已创建DCS缓存实例,且状态为“运行中”。
  • 客户端所在服务器与DCS缓存实例网络互通:
    • 客户端与Redis实例所在VPC为同一VPC

      同一VPC内网络默认互通。

    • 客户端与Redis实例所在VPC为相同region下的不同VPC

      如果客户端与Redis实例不在相同VPC中,可以通过建立VPC对等连接方式连通网络,具体请参考:《分布式缓存服务用户指南》中的“常见问题>DCS实例是否支持跨VPC访问?”章节。

    • 客户端与Redis实例所在VPC不在相同region

      如果客户端服务器和Redis实例不在同一region,仅支持通过云专线打通网络,请参考《云专线服务用户指南》。

  • 客户端所在的服务器已安装JDK1.8以上版本和开发工具(本文档以安装Eclipse为例),下载jedis客户端(单击此处直接下载jar包)。

    本文档下载的开发工具和客户端仅为示例,您可以选择其它类型的工具和客户端。

实施步骤

  1. 在服务器上运行Eclipse,创建一个java工程,为示例代码分别创建一个分布式锁实现类DistributedLock.java和测试类CaseTest.java,并将jedis客户端作为library引用到工程中。

    创建的分布式锁实现类DistributedLock.java内容示例如下:

    package dcsDemo01;
    
    import java.util.UUID;
    
    import redis.clients.jedis.Jedis;
    import redis.clients.jedis.params.SetParams;
    
    public class DistributedLock {
        // Redis实例连接地址和端口,需替换为实际获取的值
        private final String host = "192.168.0.220";
        private final int port = 6379;
    
        private static final String SUCCESS = "OK";
    
        public  DistributedLock(){}
    
        /*
         * @param lockName      锁名
         * @param timeout       获取锁的超时时间
         * @param lockTimeout   锁的有效时间
         * @return              锁的标识
         */
        public String getLockWithTimeout(String lockName, long timeout, long lockTimeout) {
            String ret = null;
            Jedis jedisClient = new Jedis(host, port);
    
            try {
                // Redis实例连接密码,需替换为实际获取的值
                String authMsg = jedisClient.auth("passwd");
                if (!SUCCESS.equals(authMsg)) {
                    System.out.println("AUTH FAILED: " + authMsg);
                }
    
                String identifier = UUID.randomUUID().toString();
                String lockKey = "DLock:" + lockName;
                long end = System.currentTimeMillis() + timeout;
    
                SetParams setParams = new SetParams();
                setParams.nx().px(lockTimeout);
    
                while(System.currentTimeMillis() < end) {
                    String result = jedisClient.set(lockKey, identifier, setParams);
                    if(SUCCESS.equals(result)) {
                        ret = identifier;
                        break;
                    }
    
                    try {
                        Thread.sleep(2);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
            }
            catch (Exception e) {
                e.printStackTrace();
            }finally {
                jedisClient.quit();
                jedisClient.close();
            }
    
            return ret;
        }
    
        /*
         * @param lockName        锁名
         * @param identifier    锁的标识
         */
        public void releaseLock(String lockName, String identifier) {
            Jedis jedisClient = new Jedis(host, port);
    
            try {
                String authMsg = jedisClient.auth("passwd");
                if (!SUCCESS.equals(authMsg)) {
                    System.out.println("AUTH FAILED: " + authMsg);
                }
    
                String lockKey = "DLock:" + lockName;
                if(identifier.equals(jedisClient.get(lockKey))) {
                    jedisClient.del(lockKey);
                }
            }
            catch (Exception e) {
                e.printStackTrace();
            }finally {
                jedisClient.quit();
                jedisClient.close();
            }
        }
    }

    该代码实现仅展示使用DCS服务进行加锁访问的便捷性。具体技术实现需要考虑死锁、锁的检查等情况,这里不做详细说明。

    假设20个线程对10台mate10手机进行抢购,创建的测试类CaseTest.java类内容示例如下:
    package dcsDemo01;
    import java.util.UUID;
    
    public class CaseTest {
        public static void main(String[] args) {
            ServiceOrder service = new ServiceOrder();
            for (int i = 0; i < 20; i++) {
                ThreadBuy client = new ThreadBuy(service);
                client.start();
            }
        }
    }
    
    class ServiceOrder {
        private final int MAX = 10;
    
        DistributedLock DLock = new DistributedLock();
    
        int n = 10;
    
        public void handleOder() {
            String userName = UUID.randomUUID().toString().substring(0,8) + Thread.currentThread().getName();
            String identifier = DLock.getLockWithTimeout("Mate 10", 10000, 2000);
            System.out.println("正在为用户:" + userName + " 处理订单");
            if(n > 0) {
                int num = MAX - n + 1;
                System.out.println("用户:"+ userName + "购买第" + num + "台,剩余" + (--n) + "台");
            }else {
                System.out.println("用户:"+ userName + "无法购买!");
            }
            DLock.releaseLock("Mate 10", identifier);
        }
    }
    
    class ThreadBuy extends Thread {
        private ServiceOrder service;
    
        public ThreadBuy(ServiceOrder service) {
            this.service = service;
        }
    
        @Override
        public void run() {
            service.handleOder();
        }
    }

  2. 将DCS缓存实例的连接地址、端口以及连接密码配置到分布式锁实现类DistributedLock.java示例代码文件中。

    在DistributedLock.java中,host及port配置为实例的连接地址及端口号,在getLockWithTimeout、releaseLock方法中需配置passwd值为实例访问密码。

  3. 将测试类CaseTest中加锁部分注释掉,变成无锁情况,示例如下:

    //测试类中注释两行用于加锁的代码:
    public void handleOder() {
        String userName = UUID.randomUUID().toString().substring(0,8) + Thread.currentThread().getName();
        //加锁代码
        //String identifier = DLock.getLockWithTimeout("Mate 10", 10000, 2000);
        System.out.println("正在为用户:" + userName + " 处理订单");
        if(n > 0) {
            int num = MAX - n + 1;
            System.out.println("用户:"+ userName + "购买第" + num + "台,剩余" + (--n) + "台");
        }else {
            System.out.println("用户:"+ userName + "无法购买!");
        }
        //加锁代码
        //DLock.releaseLock("Mate 10", identifier);
    }

  4. 编译及运行无锁的类,运行结果是抢购无序的,如下:

    正在为用户:e04934ddThread-5 处理订单
    正在为用户:a4554180Thread-0 处理订单
    用户:a4554180Thread-0购买第2台,剩余8台
    正在为用户:b58eb811Thread-10 处理订单
    用户:b58eb811Thread-10购买第3台,剩余7台
    正在为用户:e8391c0eThread-19 处理订单
    正在为用户:21fd133aThread-13 处理订单
    正在为用户:1dd04ff4Thread-6 处理订单
    用户:1dd04ff4Thread-6购买第6台,剩余4台
    正在为用户:e5977112Thread-3 处理订单
    正在为用户:4d7a8a2bThread-4 处理订单
    用户:e5977112Thread-3购买第7台,剩余3台
    正在为用户:18967410Thread-15 处理订单
    用户:18967410Thread-15购买第9台,剩余1台
    正在为用户:e4f51568Thread-14 处理订单
    用户:21fd133aThread-13购买第5台,剩余5台
    用户:e8391c0eThread-19购买第4台,剩余6台
    正在为用户:d895d3f1Thread-12 处理订单
    用户:d895d3f1Thread-12无法购买!
    正在为用户:7b8d2526Thread-11 处理订单
    用户:7b8d2526Thread-11无法购买!
    正在为用户:d7ca1779Thread-8 处理订单
    用户:d7ca1779Thread-8无法购买!
    正在为用户:74fca0ecThread-1 处理订单
    用户:74fca0ecThread-1无法购买!
    用户:e04934ddThread-5购买第1台,剩余9台
    用户:e4f51568Thread-14购买第10台,剩余0台
    正在为用户:aae76a83Thread-7 处理订单
    用户:aae76a83Thread-7无法购买!
    正在为用户:c638d2cfThread-2 处理订单
    用户:c638d2cfThread-2无法购买!
    正在为用户:2de29a4eThread-17 处理订单
    用户:2de29a4eThread-17无法购买!
    正在为用户:40a46ba0Thread-18 处理订单
    用户:40a46ba0Thread-18无法购买!
    正在为用户:211fd9c7Thread-9 处理订单
    用户:211fd9c7Thread-9无法购买!
    正在为用户:911b83fcThread-16 处理订单
    用户:911b83fcThread-16无法购买!
    用户:4d7a8a2bThread-4购买第8台,剩余2台

  5. 取消测试类CaseTest中注释的加锁内容,编译并运行得到有序的抢购结果如下:

    正在为用户:eee56fb7Thread-16 处理订单
    用户:eee56fb7Thread-16购买第1台,剩余9台
    正在为用户:d6521816Thread-2 处理订单
    用户:d6521816Thread-2购买第2台,剩余8台
    正在为用户:d7b3b983Thread-19 处理订单
    用户:d7b3b983Thread-19购买第3台,剩余7台
    正在为用户:36a6b97aThread-15 处理订单
    用户:36a6b97aThread-15购买第4台,剩余6台
    正在为用户:9a973456Thread-1 处理订单
    用户:9a973456Thread-1购买第5台,剩余5台
    正在为用户:03f1de9aThread-14 处理订单
    用户:03f1de9aThread-14购买第6台,剩余4台
    正在为用户:2c315ee6Thread-11 处理订单
    用户:2c315ee6Thread-11购买第7台,剩余3台
    正在为用户:2b03b7c0Thread-12 处理订单
    用户:2b03b7c0Thread-12购买第8台,剩余2台
    正在为用户:75f25749Thread-0 处理订单
    用户:75f25749Thread-0购买第9台,剩余1台
    正在为用户:26c71db5Thread-18 处理订单
    用户:26c71db5Thread-18购买第10台,剩余0台
    正在为用户:c32654dbThread-17 处理订单
    用户:c32654dbThread-17无法购买!
    正在为用户:df94370aThread-7 处理订单
    用户:df94370aThread-7无法购买!
    正在为用户:0af94cddThread-5 处理订单
    用户:0af94cddThread-5无法购买!
    正在为用户:e52428a4Thread-13 处理订单
    用户:e52428a4Thread-13无法购买!
    正在为用户:46f91208Thread-10 处理订单
    用户:46f91208Thread-10无法购买!
    正在为用户:e0ca87bbThread-9 处理订单
    用户:e0ca87bbThread-9无法购买!
    正在为用户:f385af9aThread-8 处理订单
    用户:f385af9aThread-8无法购买!
    正在为用户:46c5f498Thread-6 处理订单
    用户:46c5f498Thread-6无法购买!
    正在为用户:935e0f50Thread-3 处理订单
    用户:935e0f50Thread-3无法购买!
    正在为用户:d3eaae29Thread-4 处理订单
    用户:d3eaae29Thread-4无法购买!