使用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
- 客户端与Redis实例所在VPC为相同region下的不同VPC
如果客户端与Redis实例不在相同VPC中,可以通过建立VPC对等连接方式连通网络,具体请参考:缓存实例是否支持跨VPC访问?。
- 客户端与Redis实例所在VPC不在相同region
如果客户端服务器和Redis实例不在同一region,仅支持通过云专线打通网络,请参考云专线。
- 公网访问
客户端公网访问Redis 4.0/5.0/6.0实例请参考使用Nginx实现公网访问DCS或使用华为云ELB公网访问DCS。
- 客户端所在的服务器已安装JDK1.8以上版本和开发工具(本文档以安装Eclipse为例),下载jedis客户端(单击此处直接下载jar包)。
本文档下载的开发工具和客户端仅为示例,您可以选择其它类型的工具和客户端。
实施步骤
- 在服务器上运行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(); } }
- 将DCS缓存实例的连接地址、端口以及连接密码配置到分布式锁实现类DistributedLock.java示例代码文件中。
在DistributedLock.java中,host及port配置为实例的连接地址及端口号,在getLockWithTimeout、releaseLock方法中需配置passwd值为实例访问密码。
- 将测试类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); }
- 编译及运行无锁的类,运行结果是抢购无序的,如下:
正在为用户: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台
- 取消测试类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无法购买!