文档首页/ 分布式缓存服务 DCS/ 最佳实践/ 业务应用/ 使用DCS实现电商秒杀功能
更新时间:2024-10-24 GMT+08:00

使用DCS实现电商秒杀功能

方案概述

应用场景

电商秒杀是一种网上竞拍活动,通常商家会在平台释放少量稀缺商品,吸引大量客户,平台会收到平时数十倍甚至上百倍的下单请求,但是只有少数客户可以下单成功。电商秒杀系统的分流过程可以分为以下几个步骤:

  1. 用户请求进入系统:当用户发起秒杀请求时,请求会首先进入负载均衡服务器。
  2. 负载均衡:负载均衡服务器会根据一定的算法将请求分发给后端多台服务器,以达到负载均衡的目的。负载均衡算法可以采用轮询、随机、最少连接数等方式。
  3. 业务逻辑处理:后端服务器接收到请求后,进行业务逻辑处理,并根据请求的商品数量、用户身份等信息进行校验。
  4. 库存扣减:如果库存充足,后端服务器会进行库存扣减操作,并生成订单信息,返回给用户秒杀成功的信息;如果库存不足,则返回给用户秒杀失败的信息。
  5. 订单处理:后端服务器会将订单信息保存到数据库中,并进行异步处理,例如发送消息通知用户订单状态。
  6. 缓存更新:后端服务器会更新缓存中的商品库存信息,以便处理下一次秒杀请求。

秒杀过程中多次访问数据库,下单通常是利用行级锁进行访问限制,抢到锁才能查询数据库和下单。但是秒杀时的大量订单请求,会导致数据库访问阻塞。

解决方案

利用分布式缓存服务(DCS)的Redis作为数据库的缓存,客户端访问Redis进行库存查询和下单操作,具有以下优势:

  • Redis提供很高的读写速度和并发性能,可以满足电商秒杀系统高并发的需求。
  • Redis支持主备、集群等高可用架构, 支持数据持久化,即使服务器宕机也可以恢复数据。
  • Redis支持事务和原子性操作,可以保证秒杀操作的一致性和正确性。
  • 利用Redis缓存商品和用户信息,减轻数据库的压力,提高系统的性能。

本篇文档示例中,用Redis中的hash结构表示商品信息。total表示总数,booked表示下单数,remain表示剩余商品数量。

“product”: {
“total”: 200
“booked”:0
“remain”:200
}

扣量时,服务器通过请求Redis获取下单资格。Redis为单线程模型,lua可以保证多个命令的原子性。通过如下lua脚本完成扣量。

local n = tonumber(ARGV[1])
if not n  or n == 0 then
   return 0
end
local vals = redis.call(\"HMGET\", KEYS[1], \"total\", \"booked\", \"remain\");
local booked = tonumber(vals[2])
local remain = tonumber(vals[3])
if booked <= remain then
   redis.call(\"HINCRBY\", KEYS[1], \"booked\", n)
   redis.call(\"HINCRBY\", KEYS[1], \"remain\", -n)
   return n;
end
return 0

前提条件

  • 已创建DCS缓存实例,且状态为“运行中”。
  • 客户端所在服务器与DCS缓存实例网络互通:
    • 客户端与Redis实例所在VPC为同一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以上版本和Intellij IDEA开发工具,下载jedis客户端(点此处下载jar包)。

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

实施步骤

  1. 在服务器上运行Intellij IDEA,创建一个MAVEN工程,为示例代码创建一个SecondsKill.java文件,pom.xml文件中引用Jedis:

    <dependency>
          <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
          <version>4.2.0</version>
    </dependency>

  2. 编译并运行以下demo,该示例以Java语言实现。

    示例中的Redis连接地址和端口需要根据实际获取的值进行修改。
    package com.huawei.demo;
    import java.util.ArrayList;
    import java.util.*;
    
    import redis.clients.jedis.Jedis;
    import redis.clients.jedis.JedisPool;
    import redis.clients.jedis.JedisPoolConfig;
    
    public class SecondsKill {
    	private static void InitProduct(Jedis jedis) {
    		jedis.hset("product", "total", "200");
    		jedis.hset("product", "booked", "0");
    		jedis.hset("product","remain", "200");
    	}
    
    	private static String LoadLuaScript(Jedis jedis) {
    		String lua = "local n = tonumber(ARGV[1])\n"
    			+ "if not n  or n == 0 then\n"
    			+ "return 0\n"
    			+ "end\n"
    			+ "local vals = redis.call(\"HMGET\", KEYS[1], \"total\", \"booked\", \"remain\");\n"
    			+ "local booked = tonumber(vals[2])\n"
    			+ "local remain = tonumber(vals[3])\n"
    			+ "if booked <= remain then\n"
    			+ "redis.call(\"HINCRBY\", KEYS[1], \"booked\", n)\n"
    			+ "redis.call(\"HINCRBY\", KEYS[1], \"remain\", -n)\n"
    			+ "return n;\n"
    			+ "end\n"
    			+ "return 0";
    		String scriptLoad = jedis.scriptLoad(lua);
    
    		return scriptLoad;
    	}
    
    	public static void main(String[] args) {
    		JedisPoolConfig config = new JedisPoolConfig();
    		// 最大连接数
    		config.setMaxTotal(30);
    		// 最大连接空闲数
    		config.setMaxIdle(2);
    		// 连接Redis,Redis实例连接地址和端口需替换为实际获取的值
    		JedisPool pool = new JedisPool(config, "127.0.0.1", 6379);
    		Jedis jedis = null;
    		try {
    			jedis = pool.getResource();
                            jedis.auth("password");   //配置实例的连接密码,免密访问的实例无需填写
    			System.out.println(jedis);
    
    			// 初始化产品信息
    			InitProduct(jedis);
    
    			// 存入lua脚本
    			String scriptLoad = LoadLuaScript(jedis);
    
    			List<String> keys = new ArrayList<>();
    			List<String> vals = new ArrayList<>();
    			keys.add("product");
    
    			//下单15个
    			int num = 15;
    			vals.add(String.valueOf(num));
    
    			//执行lua脚本
    			jedis.evalsha(scriptLoad, keys, vals);
    			System.out.println("total:"+jedis.hget("product", "total")+"\n"+"booked:"+jedis.hget("product",
    				"booked")+"\n"+"remain:"+jedis.hget("product","remain"));
    
    		} catch (Exception ex) {
    			ex.printStackTrace();
    		} finally {
    			if (jedis != null) {
    				jedis.close();
    			}
    		}
    	}
    }

    执行结果:

    total:200
    booked:15
    remain:185