一、简介
1 秒杀系统
秒杀系统是指在一个非常短的时间内(通常是几十秒钟),将某种商品或服务以极低的价格进行销售。这种销售方式需要保证高并发和高可用性,同时防止超卖和恶意攻击等问题。秒杀系统的特点是大量的用户在同一时间瞬间涌入服务器,该类型的高并发读写操作对系统性能提出了较高的要求。
2 常见问题
在秒杀场景下,会遇到以下常见问题:
- 高并发(每秒新建的TCP连接数非常高)
- 超卖(由于网页刷新频率过快,导致用户可购买数量超出实际剩余数量)
- 恶意攻击(攻击者通过机器人、脚本等手段进行抢购,从而瘫痪系统)
二、Redis 简介
Redis(Remote Dictionary Server)是一个开源、支持网络、基于内存、键值对存储数据库。它支持多种数据结构,如字符串(String)、哈希(Hash)、列表(List)、集合(Set)和有序集合(Sorted Set)。Redis 的访问速度非常快,在存储海量数据时,丝毫不会影响系统性能,所以 Redis 被广泛应用于高并发的互联网项目中。
1 Redis基本概念
- 单线程:Redis 采用单线程模型进行工作,避免了线程切换带来的上下文切换开销,因此速度非常快。
- 持久化存储:Redis 中支持 RDB 持久化和 AOF 持久化,可以将内存中的数据保留到磁盘上,防止服务器崩溃时数据的丢失。
- 丰富的数据类型:Redis 支持多种数据类型,如字符串、列表、集合、哈希表等,方便用户根据不同的业务需求选择合适的数据类型。
- 高性能:Redis 是一个基于内存的数据库,它的读写速度都非常快,同时也因为是基于内存,所以 Redis 的存储容量受限,不适用于存储大量的数据。
- 分布式:Redis 支持分布式集群,可以将数据进行分片存储,提高了系统的并发处理能力,同时增加了系统的可扩展性,保证了高可用性。
2 Redis 作为秒杀系统的优点
- 高效读写:Redis 的读写性能非常快,能够满足秒杀系统的高并发读写需求,保证了系统的高效运作。
- 数据持久化:Redis 支持数据的持久化存储,可以将内存中的数据保留到磁盘上,防止服务器崩溃时数据的丢失,减小对系统的影响。
- 分布式特性:Redis 支持分布式集群,可以将缓存分片存储,避免单节点压力过大,保证了系统的可扩展性和高可用性。
- 原子操作:Redis 支持多个操作的原子性,如事务处理、CAS 等,保证了数据的一致性和安全性,有效地防止了超卖等问题。
三、Redis 在秒杀系统中的应用
1 数据存储中的应用
Redis 的快速读写操作使得它成为二级缓存的首选,常用于缓存不经常变更或者不经常使用的数据。在秒杀系统中,Redis 可以用来缓存商品名称、库存数量、是否售罄等信息,减少数据库的访问量,提高数据读写效率和系统的响应速度。
// jedis 是 Redis 的 Java 客户端
// 设置 key-value 对
jedis.set("product:001:name", "iPhone 12");
jedis.set("product:001:stock", "1000");
// 获取 key-value 对
String name = jedis.get("product:001:name");
String stock = jedis.get("product:001:stock");
2 在分布式锁中的应用
在秒杀场景中为了防止商品超卖,通常需要引入分布式锁机制。Redis 提供了一种简单有效的分布式锁实现方式,通过抢占 key 来实现锁,避免了多个系统同时修改数据的情况。
// 尝试获取锁
boolean lockResult = jedis.setnx("lock:product:001", "value");
if(lockResult) {
// 获取锁成功,执行业务逻辑...
// 释放锁
jedis.del("lock:product:001");
} else {
// 获取锁失败,等待重试...
}
3 在消息队列中的应用
在秒杀场景中系统需要处理大量并发请求,为了避免请求在瞬间涌入服务器导致系统崩溃,可以使用消息队列来对用户的请求进行排队,这样可以有效地缓解系统压力。
// 将秒杀请求加入消息队列
jedis.lpush("seckill:requests", "request001");
// 从消息队列中获取请求
String request = jedis.brpop("seckill:requests", 10).get(1);
四、Redis秒杀系统设计
1 数据库表设计
秒杀系统一般需要两个表:商品表和订单表。商品表用于存储商品信息,订单表用于存储订单信息。
商品表设计
在商品表中需要包含以下字段:
字段名 | 类型 | 描述 |
---|---|---|
id | int | 商品id |
name | varchar | 商品名称 |
description | text | 商品描述 |
price | decimal | 商品单价 |
stock | int | 商品库存 |
订单表设计
在订单表中需要包含以下字段:
字段名 | 类型 | 描述 |
---|---|---|
id | int | 订单id |
user_id | int | 用户id |
goods_id | int | 商品id |
create_time | datetime | 创建时间 |
status | int | 订单状态,0表示未支付,1表示已支付 |
2 接口设计
秒杀系统需要以下几个接口:
- 商品列表接口:用于获取商品列表。
- 商品详情接口:用于获取指定商品的详细信息。
- 下单接口:用于下单操作。
- 订单列表接口:用于获取对应用户的订单列表。
3 队列设计
秒杀系统需要一个队列用于处理订单的下单请求,可以选用Redis作为队列。在Redis中使用list数据结构作为队列,在多个服务器下运行多个相同的消费者程序,以实现分布式处理订单请求。
4 Redis 优化策略
为了保证秒杀系统的高并发和性能,需要对Redis进行优化。优化策略包括:
- 增加Redis的内存大小,以缓存更多的商品和订单信息。
- 合理设置Redis的过期时间,避免Redis中的数据一直占用内存。
- 使用Redis集群模式或主从复制模式,以提高Redis的可用性和性能。
五、秒杀系统的实现流程
1 商品初始化
在秒杀系统中首先需要进行商品初始化。具体实现流程如下:
// 定义商品实体类
public class Goods {
private int id;
private String name;
private int stock;
private double price;
// 省略 getter 和 setter 方法
}
// 在系统启动时,从数据库中读取所有秒杀商品信息
List<Goods> goodsList = goodsDAO.queryAllSeckillGoods();
for (Goods goods : goodsList) {
// 将商品信息存入到 Redis 中,以便后续操作使用
redisService.set("seckill:good:" + goods.getId(), JSON.toJSONString(goods));
// 将商品库存数量存入到 Redis 中,以便进行库存的修改操作
redisService.set("seckill:stock:" + goods.getId(), goods.getStock());
}
5.2 前端页面限流
在秒杀系统中,为了避免瞬间大量用户访问导致系统崩溃,需要对前端页面进行限流。具体实现流程如下:
// 在前端页面中加入验证码或者滑动验证等机制
public class SeckillController {
@PostMapping("/seckill")
public String seckill(@RequestParam("goodsId") int goodsId,
@RequestParam("userId") int userId,
@RequestParam("verifyCode") String verifyCode) {
// 验证码通过之后再执行秒杀操作
if (verifyCodeIsValid(userId, verifyCode)) {
// 秒杀操作
seckillService.seckill(goodsId, userId);
}
}
}
5.3 后端请求接口限流
在秒杀系统中,同样需要对后端请求接口进行限流,以避免恶意攻击。具体实现流程如下:
// 使用限流工具对后端接口进行限流
public class SeckillController {
@PostMapping("/seckill")
public String seckill(@RequestParam("goodsId") int goodsId,
@RequestParam("userId") int userId) {
if (rateLimiter.tryAcquire()) { // 使用 Guava RateLimiter 进行限流
// 秒杀操作
seckillService.seckill(goodsId, userId);
} else {
return "请求过于频繁,请稍后再试!";
}
}
}
5.4 分布式锁控制全局唯一性
在秒杀系统中由于多个用户同时访问同一个商品,需要对商品进行加锁,保证全局唯一性。具体实现流程如下:
// 使用 Redis 的分布式锁实现秒杀商品的唯一性
public class SeckillServiceImpl implements SeckillService {
@Override
public void seckill(int goodsId, int userId) {
// 加锁操作
String lockKey = "seckill:lock:" + goodsId;
String requestId = UUID.randomUUID().toString();
long expireTime = 3000; // 锁过期时间设置为 3 秒钟
boolean isSuccess = redisService.tryLock(lockKey, requestId, expireTime);
if (isSuccess) {
try {
// 秒杀操作
int stock = redisService.get("seckill:stock:" + goodsId, Integer.class);
if (stock > 0) {
redisService.decr("seckill:stock:" + goodsId); // 减库存
seckillDAO.insertOrder(goodsId, userId); // 写入订单记录
notificationService.sendSeckillSuccessMsg(userId, goodsId); // 发送通知消息
}
} finally {
// 释放锁操作
redisService.releaseLock(lockKey, requestId);
}
}
}
}
5.5 Redis 减库存
在秒杀系统中对商品的操作都是基于 Redis 获取和修改的,包括商品库存数量。具体实现流程如下:
// Redis 减库存操作
public class SeckillServiceImpl implements SeckillService {
@Override
public void seckill(int goodsId, int userId) {
// 加锁和减库存操作
int stock = redisService.get("seckill:stock:" + goodsId, Integer.class);
if (stock > 0) {
redisService.decr("seckill:stock:" + goodsId);
// 省略其他业务逻辑操作
}
}
}
5.6 MySQL 写入订单记录
在秒杀系统中,需要将成功秒杀的订单信息记录到 MySQL 数据库中。具体实现流程如下:
// MySQL 写入订单记录操作
public class SeckillDAOImpl implements SeckillDAO {
@Override
public void insertOrder(int goodsId, int userId) {
String sql = "INSERT INTO seckill_order (goods_id, user_id, create_time) VALUES (?, ?, ?)";
jdbcTemplate.update(sql, goodsId, userId, new Date());
}
}
5.7 消息通知用户秒杀成功
在秒杀系统中可以通过消息队列等方式,对用户进行秒杀成功的通知。具体实现流程如下:
// 消息通知用户秒杀成功
public class NotificationServiceImpl implements NotificationService {
private static final Logger logger = LoggerFactory.getLogger(NotificationServiceImpl.class);
@Override
public void sendSeckillSuccessMsg(int userId, int goodsId) {
// 使用消息队列对用户进行通知
Message message = new Message();
message.setUserId(userId);
message.setGoodsId(goodsId);
rocketMQTemplate.convertAndSend("seckill-success-topic", message);
logger.info("通知消息已发送:{}", message);
}
}
六、安全策略
秒杀系统是一个高并发业务,为了保证系统的安全性和稳定性,在使用Redis做缓存的同时,需要针对以下两个方面进行安全策略的设计:
1 防止超卖
在秒杀活动中,一件商品仅有有限的数量,当超过了这个数量之后就不能再销售,此时需要采取防止超卖的措施。
实现方式
- 基于Redis的单线程机制,把减库存操作原子化执行,并且需要锁住对应的商品id。
- 针对锁定商品的情况,使用 Redis 的分布式锁机制。以此来保证一次只有一个请求能够成功地请求到库存锁,并且持有锁的时间应尽量短。
下面是Java代码实现:文章来源地址https://www.toymoban.com/news/detail-510479.html
public boolean decrementStock(String key) {
String lockKey = "LOCK_" + key;
try (Jedis jedis = jedisPool.getResource()) {
//加锁
String lockValue = UUID.randomUUID().toString();
String result;
while (true) {
result = jedis.set(lockKey, lockValue, "NX", "PX", 3000);
if ("OK".equals(result)) {
break;
}
Thread.sleep(100);
}
//判断是否加锁成功
if (!lockValue.equals(jedis.get(lockKey))) {
return false;
}
try {
//操作库存
int stock = Integer.parseInt(jedis.get(key));
if (stock > 0) {
jedis.decr(key);
return true;
}
return false;
} finally {
//释放锁
jedis.del(lockKey);
}
} catch (Exception e) {
log.error("decrementStock failed, key:{}", key, e);
return false;
}
}
2 防止恶意刷单
恶意用户通过程序模拟大量请求,从而导致服务器无法响应正常用户的请求。为了解决这个问题,需要加入防止恶意刷单的策略。文章来源:https://www.toymoban.com/news/detail-510479.html
实现方式
- 对每个用户IP进行限流,设置每分钟能够请求的次数。
- 设置人机验证,如图形验证码或者短信验证等机制,让恶意用户成本太高从而放弃攻击。
下面是Java代码实现:
public boolean checkUserRequest(String ip) {
// 检查ip对应的请求数是否超过最大允许请求次数
String requestCountKey = "REQUEST_COUNT_" + ip;
try (Jedis jedis = jedisPool.getResource()) {
long currentCount = jedis.incr(requestCountKey);
if (currentCount == 1) {
// 第一次计数,设置过期时间为60s
jedis.expire(requestCountKey, 60);
}
if (currentCount > maxRequestPerMinute) {
// 超过最大允许请求次数,返回false
return false;
}
return true;
}
}
七、部署方案
1 安全性优化
- 部署到专门的CDN缓存服务器,减小服务器带宽压力,保护服务器和数据库。
- 设置服务器防火墙,禁止外部访问 Redis 和 数据库等敏感资源。
- 开启Redis的持久化,以防止内存数据丢失或者意外宕机等情况。
2 性能优化
- 提高Redis性能,采用集群方式,增加机器和配置Redis相关参数等。
- 使用高效的缓存查询方式,避免频繁查询数据库,如使用Redis自带的哈希表来存储秒杀商品信息。
到了这里,关于使用 Redis 实现秒杀系统的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!