
# 使用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对等连接方式连通网络，具体请参考：[缓存实例是否支持跨VPC访问？](https://support.huaweicloud.com/dcs_faq/dcs-faq-0427002.html)。
    
  
  - 客户端与Redis实例所在VPC不在相同region 如果客户端服务器和Redis实例不在同一region，仅支持通过云专线打通网络，请参考[云专线](https://support.huaweicloud.com/productdesc-dc/zh-cn_topic_0032053183.html)。
    
  
  - 公网访问 客户端公网访问Redis 4.0/5.0/6.0实例时，需要开启实例公网访问开关，具体请参考[开启Redis 4.0/5.0/6.0公网访问并获取公网访问地址](https://support.huaweicloud.com/usermanual-dcs/dcs_03_0011.html)。
    
   
- 客户端所在的服务器已安装[JDK1.8](https://www.oracle.com/java/technologies/downloads/)以上版本和开发工具（本文档以安装[Eclipse](https://www.eclipse.org/downloads/)为例），下载jedis客户端（[单击此处直接下载jar包](https://jar-download.com/artifacts/redis.clients/jedis/3.5.1/source-code)）。
  本文档下载的开发工具和客户端仅为示例，您可以选择其它类型的工具和客户端。
  
 
#### 实施步骤
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();
           }
       }
   }
   ```
   ![](https://support.huaweicloud.com/bestpractice-dcs/public_sys-resources/notice_3.0-zh-cn.png)
   该代码实现仅展示使用DCS服务进行加锁访问的便捷性。具体技术实现需要考虑死锁、锁的检查等情况，这里不做详细说明。
   假设20个线程对10台手机进行抢购，创建的测试类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 handleOrder() {
           String userName = UUID.randomUUID().toString().substring(0,8) + Thread.currentThread().getName();
           String identifier = DLock.getLockWithTimeout("mobile phone", 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("mobile phone", identifier);
       }
   }
   class ThreadBuy extends Thread {
       private ServiceOrder service;
       public ThreadBuy(ServiceOrder service) {
           this.service = service;
       }
       @Override
       public void run() {
           service.handleOrder();
       }
   }
   ```
   
   
2. 将DCS缓存实例的连接地址、端口以及连接密码配置到分布式锁实现类DistributedLock.java示例代码文件中。 
   在DistributedLock.java中，host及port配置为实例的连接地址及端口号，在getLockWithTimeout、releaseLock方法中需配置passwd值为实例访问密码。
   
   
3. 将测试类CaseTest中加锁部分注释掉，变成无锁情况，示例如下： 
   ```
   //测试类中注释两行用于加锁的代码：
   public void handleOrder() {
       String userName = UUID.randomUUID().toString().substring(0,8) + Thread.currentThread().getName();
       //加锁代码
       //String identifier = DLock.getLockWithTimeout("mobile phone", 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("mobile phone", 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无法购买，已售罄！
   ```
   
   
 
