更新时间:2024-10-24 GMT+08:00
使用DCS实现电商秒杀功能
方案概述
应用场景
电商秒杀是一种网上竞拍活动,通常商家会在平台释放少量稀缺商品,吸引大量客户,平台会收到平时数十倍甚至上百倍的下单请求,但是只有少数客户可以下单成功。电商秒杀系统的分流过程可以分为以下几个步骤:
- 用户请求进入系统:当用户发起秒杀请求时,请求会首先进入负载均衡服务器。
- 负载均衡:负载均衡服务器会根据一定的算法将请求分发给后端多台服务器,以达到负载均衡的目的。负载均衡算法可以采用轮询、随机、最少连接数等方式。
- 业务逻辑处理:后端服务器接收到请求后,进行业务逻辑处理,并根据请求的商品数量、用户身份等信息进行校验。
- 库存扣减:如果库存充足,后端服务器会进行库存扣减操作,并生成订单信息,返回给用户秒杀成功的信息;如果库存不足,则返回给用户秒杀失败的信息。
- 订单处理:后端服务器会将订单信息保存到数据库中,并进行异步处理,例如发送消息通知用户订单状态。
- 缓存更新:后端服务器会更新缓存中的商品库存信息,以便处理下一次秒杀请求。
秒杀过程中多次访问数据库,下单通常是利用行级锁进行访问限制,抢到锁才能查询数据库和下单。但是秒杀时的大量订单请求,会导致数据库访问阻塞。
解决方案
利用分布式缓存服务(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
- 客户端与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包)。
本文档下载的开发工具和客户端仅为示例,您可以选择其它类型的工具和客户端。
实施步骤
- 在服务器上运行Intellij IDEA,创建一个MAVEN工程,为示例代码创建一个SecondsKill.java文件,pom.xml文件中引用Jedis:
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>4.2.0</version> </dependency>
- 编译并运行以下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
父主题: 业务应用