一、簡(jiǎn)介
1 秒殺系統(tǒng)
秒殺系統(tǒng)是指在一個(gè)非常短的時(shí)間內(nèi)(通常是幾十秒鐘),將某種商品或服務(wù)以極低的價(jià)格進(jìn)行銷售。這種銷售方式需要保證高并發(fā)和高可用性,同時(shí)防止超賣和惡意攻擊等問題。秒殺系統(tǒng)的特點(diǎn)是大量的用戶在同一時(shí)間瞬間涌入服務(wù)器,該類型的高并發(fā)讀寫操作對(duì)系統(tǒng)性能提出了較高的要求。
2 常見問題
在秒殺場(chǎng)景下,會(huì)遇到以下常見問題:
- 高并發(fā)(每秒新建的TCP連接數(shù)非常高)
- 超賣(由于網(wǎng)頁刷新頻率過快,導(dǎo)致用戶可購買數(shù)量超出實(shí)際剩余數(shù)量)
- 惡意攻擊(攻擊者通過機(jī)器人、腳本等手段進(jìn)行搶購,從而癱瘓系統(tǒng))
二、Redis 簡(jiǎn)介
Redis(Remote Dictionary Server)是一個(gè)開源、支持網(wǎng)絡(luò)、基于內(nèi)存、鍵值對(duì)存儲(chǔ)數(shù)據(jù)庫。它支持多種數(shù)據(jù)結(jié)構(gòu),如字符串(String)、哈希(Hash)、列表(List)、集合(Set)和有序集合(Sorted Set)。Redis 的訪問速度非??欤诖鎯?chǔ)海量數(shù)據(jù)時(shí),絲毫不會(huì)影響系統(tǒng)性能,所以 Redis 被廣泛應(yīng)用于高并發(fā)的互聯(lián)網(wǎng)項(xiàng)目中。
1 Redis基本概念
- 單線程:Redis 采用單線程模型進(jìn)行工作,避免了線程切換帶來的上下文切換開銷,因此速度非??臁?/li>
- 持久化存儲(chǔ):Redis 中支持 RDB 持久化和 AOF 持久化,可以將內(nèi)存中的數(shù)據(jù)保留到磁盤上,防止服務(wù)器崩潰時(shí)數(shù)據(jù)的丟失。
- 豐富的數(shù)據(jù)類型:Redis 支持多種數(shù)據(jù)類型,如字符串、列表、集合、哈希表等,方便用戶根據(jù)不同的業(yè)務(wù)需求選擇合適的數(shù)據(jù)類型。
- 高性能:Redis 是一個(gè)基于內(nèi)存的數(shù)據(jù)庫,它的讀寫速度都非??欤瑫r(shí)也因?yàn)槭腔趦?nèi)存,所以 Redis 的存儲(chǔ)容量受限,不適用于存儲(chǔ)大量的數(shù)據(jù)。
- 分布式:Redis 支持分布式集群,可以將數(shù)據(jù)進(jìn)行分片存儲(chǔ),提高了系統(tǒng)的并發(fā)處理能力,同時(shí)增加了系統(tǒng)的可擴(kuò)展性,保證了高可用性。
2 Redis 作為秒殺系統(tǒng)的優(yōu)點(diǎn)
- 高效讀寫:Redis 的讀寫性能非???,能夠滿足秒殺系統(tǒng)的高并發(fā)讀寫需求,保證了系統(tǒng)的高效運(yùn)作。
- 數(shù)據(jù)持久化:Redis 支持?jǐn)?shù)據(jù)的持久化存儲(chǔ),可以將內(nèi)存中的數(shù)據(jù)保留到磁盤上,防止服務(wù)器崩潰時(shí)數(shù)據(jù)的丟失,減小對(duì)系統(tǒng)的影響。
- 分布式特性:Redis 支持分布式集群,可以將緩存分片存儲(chǔ),避免單節(jié)點(diǎn)壓力過大,保證了系統(tǒng)的可擴(kuò)展性和高可用性。
- 原子操作:Redis 支持多個(gè)操作的原子性,如事務(wù)處理、CAS 等,保證了數(shù)據(jù)的一致性和安全性,有效地防止了超賣等問題。
三、Redis 在秒殺系統(tǒng)中的應(yīng)用
1 數(shù)據(jù)存儲(chǔ)中的應(yīng)用
Redis 的快速讀寫操作使得它成為二級(jí)緩存的首選,常用于緩存不經(jīng)常變更或者不經(jīng)常使用的數(shù)據(jù)。在秒殺系統(tǒng)中,Redis 可以用來緩存商品名稱、庫存數(shù)量、是否售罄等信息,減少數(shù)據(jù)庫的訪問量,提高數(shù)據(jù)讀寫效率和系統(tǒng)的響應(yīng)速度。
// jedis 是 Redis 的 Java 客戶端
// 設(shè)置 key-value 對(duì)
jedis.set("product:001:name", "iPhone 12");
jedis.set("product:001:stock", "1000");
// 獲取 key-value 對(duì)
String name = jedis.get("product:001:name");
String stock = jedis.get("product:001:stock");
2 在分布式鎖中的應(yīng)用
在秒殺場(chǎng)景中為了防止商品超賣,通常需要引入分布式鎖機(jī)制。Redis 提供了一種簡(jiǎn)單有效的分布式鎖實(shí)現(xiàn)方式,通過搶占 key 來實(shí)現(xiàn)鎖,避免了多個(gè)系統(tǒng)同時(shí)修改數(shù)據(jù)的情況。
// 嘗試獲取鎖
boolean lockResult = jedis.setnx("lock:product:001", "value");
if(lockResult) {
// 獲取鎖成功,執(zhí)行業(yè)務(wù)邏輯...
// 釋放鎖
jedis.del("lock:product:001");
} else {
// 獲取鎖失敗,等待重試...
}
3 在消息隊(duì)列中的應(yīng)用
在秒殺場(chǎng)景中系統(tǒng)需要處理大量并發(fā)請(qǐng)求,為了避免請(qǐng)求在瞬間涌入服務(wù)器導(dǎo)致系統(tǒng)崩潰,可以使用消息隊(duì)列來對(duì)用戶的請(qǐng)求進(jìn)行排隊(duì),這樣可以有效地緩解系統(tǒng)壓力。
// 將秒殺請(qǐng)求加入消息隊(duì)列
jedis.lpush("seckill:requests", "request001");
// 從消息隊(duì)列中獲取請(qǐng)求
String request = jedis.brpop("seckill:requests", 10).get(1);
四、Redis秒殺系統(tǒng)設(shè)計(jì)
1 數(shù)據(jù)庫表設(shè)計(jì)
秒殺系統(tǒng)一般需要兩個(gè)表:商品表和訂單表。商品表用于存儲(chǔ)商品信息,訂單表用于存儲(chǔ)訂單信息。
商品表設(shè)計(jì)
在商品表中需要包含以下字段:
字段名 | 類型 | 描述 |
---|---|---|
id | int | 商品id |
name | varchar | 商品名稱 |
description | text | 商品描述 |
price | decimal | 商品單價(jià) |
stock | int | 商品庫存 |
訂單表設(shè)計(jì)
在訂單表中需要包含以下字段:
字段名 | 類型 | 描述 |
---|---|---|
id | int | 訂單id |
user_id | int | 用戶id |
goods_id | int | 商品id |
create_time | datetime | 創(chuàng)建時(shí)間 |
status | int | 訂單狀態(tài),0表示未支付,1表示已支付 |
2 接口設(shè)計(jì)
秒殺系統(tǒng)需要以下幾個(gè)接口:
- 商品列表接口:用于獲取商品列表。
- 商品詳情接口:用于獲取指定商品的詳細(xì)信息。
- 下單接口:用于下單操作。
- 訂單列表接口:用于獲取對(duì)應(yīng)用戶的訂單列表。
3 隊(duì)列設(shè)計(jì)
秒殺系統(tǒng)需要一個(gè)隊(duì)列用于處理訂單的下單請(qǐng)求,可以選用Redis作為隊(duì)列。在Redis中使用list數(shù)據(jù)結(jié)構(gòu)作為隊(duì)列,在多個(gè)服務(wù)器下運(yùn)行多個(gè)相同的消費(fèi)者程序,以實(shí)現(xiàn)分布式處理訂單請(qǐng)求。
4 Redis 優(yōu)化策略
為了保證秒殺系統(tǒng)的高并發(fā)和性能,需要對(duì)Redis進(jìn)行優(yōu)化。優(yōu)化策略包括:
- 增加Redis的內(nèi)存大小,以緩存更多的商品和訂單信息。
- 合理設(shè)置Redis的過期時(shí)間,避免Redis中的數(shù)據(jù)一直占用內(nèi)存。
- 使用Redis集群模式或主從復(fù)制模式,以提高Redis的可用性和性能。
五、秒殺系統(tǒng)的實(shí)現(xiàn)流程
1 商品初始化
在秒殺系統(tǒng)中首先需要進(jìn)行商品初始化。具體實(shí)現(xiàn)流程如下:
// 定義商品實(shí)體類
public class Goods {
private int id;
private String name;
private int stock;
private double price;
// 省略 getter 和 setter 方法
}
// 在系統(tǒng)啟動(dòng)時(shí),從數(shù)據(jù)庫中讀取所有秒殺商品信息
List<Goods> goodsList = goodsDAO.queryAllSeckillGoods();
for (Goods goods : goodsList) {
// 將商品信息存入到 Redis 中,以便后續(xù)操作使用
redisService.set("seckill:good:" + goods.getId(), JSON.toJSONString(goods));
// 將商品庫存數(shù)量存入到 Redis 中,以便進(jìn)行庫存的修改操作
redisService.set("seckill:stock:" + goods.getId(), goods.getStock());
}
5.2 前端頁面限流
在秒殺系統(tǒng)中,為了避免瞬間大量用戶訪問導(dǎo)致系統(tǒng)崩潰,需要對(duì)前端頁面進(jìn)行限流。具體實(shí)現(xiàn)流程如下:
// 在前端頁面中加入驗(yàn)證碼或者滑動(dòng)驗(yàn)證等機(jī)制
public class SeckillController {
@PostMapping("/seckill")
public String seckill(@RequestParam("goodsId") int goodsId,
@RequestParam("userId") int userId,
@RequestParam("verifyCode") String verifyCode) {
// 驗(yàn)證碼通過之后再執(zhí)行秒殺操作
if (verifyCodeIsValid(userId, verifyCode)) {
// 秒殺操作
seckillService.seckill(goodsId, userId);
}
}
}
5.3 后端請(qǐng)求接口限流
在秒殺系統(tǒng)中,同樣需要對(duì)后端請(qǐng)求接口進(jìn)行限流,以避免惡意攻擊。具體實(shí)現(xiàn)流程如下:
// 使用限流工具對(duì)后端接口進(jìn)行限流
public class SeckillController {
@PostMapping("/seckill")
public String seckill(@RequestParam("goodsId") int goodsId,
@RequestParam("userId") int userId) {
if (rateLimiter.tryAcquire()) { // 使用 Guava RateLimiter 進(jìn)行限流
// 秒殺操作
seckillService.seckill(goodsId, userId);
} else {
return "請(qǐng)求過于頻繁,請(qǐng)稍后再試!";
}
}
}
5.4 分布式鎖控制全局唯一性
在秒殺系統(tǒng)中由于多個(gè)用戶同時(shí)訪問同一個(gè)商品,需要對(duì)商品進(jìn)行加鎖,保證全局唯一性。具體實(shí)現(xiàn)流程如下:
// 使用 Redis 的分布式鎖實(shí)現(xiàn)秒殺商品的唯一性
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; // 鎖過期時(shí)間設(shè)置為 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); // 發(fā)送通知消息
}
} finally {
// 釋放鎖操作
redisService.releaseLock(lockKey, requestId);
}
}
}
}
5.5 Redis 減庫存
在秒殺系統(tǒng)中對(duì)商品的操作都是基于 Redis 獲取和修改的,包括商品庫存數(shù)量。具體實(shí)現(xiàn)流程如下:
// 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);
// 省略其他業(yè)務(wù)邏輯操作
}
}
}
5.6 MySQL 寫入訂單記錄
在秒殺系統(tǒng)中,需要將成功秒殺的訂單信息記錄到 MySQL 數(shù)據(jù)庫中。具體實(shí)現(xiàn)流程如下:
// 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 消息通知用戶秒殺成功
在秒殺系統(tǒng)中可以通過消息隊(duì)列等方式,對(duì)用戶進(jìn)行秒殺成功的通知。具體實(shí)現(xiàn)流程如下:
// 消息通知用戶秒殺成功
public class NotificationServiceImpl implements NotificationService {
private static final Logger logger = LoggerFactory.getLogger(NotificationServiceImpl.class);
@Override
public void sendSeckillSuccessMsg(int userId, int goodsId) {
// 使用消息隊(duì)列對(duì)用戶進(jìn)行通知
Message message = new Message();
message.setUserId(userId);
message.setGoodsId(goodsId);
rocketMQTemplate.convertAndSend("seckill-success-topic", message);
logger.info("通知消息已發(fā)送:{}", message);
}
}
六、安全策略
秒殺系統(tǒng)是一個(gè)高并發(fā)業(yè)務(wù),為了保證系統(tǒng)的安全性和穩(wěn)定性,在使用Redis做緩存的同時(shí),需要針對(duì)以下兩個(gè)方面進(jìn)行安全策略的設(shè)計(jì):
1 防止超賣
在秒殺活動(dòng)中,一件商品僅有有限的數(shù)量,當(dāng)超過了這個(gè)數(shù)量之后就不能再銷售,此時(shí)需要采取防止超賣的措施。
實(shí)現(xiàn)方式
- 基于Redis的單線程機(jī)制,把減庫存操作原子化執(zhí)行,并且需要鎖住對(duì)應(yīng)的商品id。
- 針對(duì)鎖定商品的情況,使用 Redis 的分布式鎖機(jī)制。以此來保證一次只有一個(gè)請(qǐng)求能夠成功地請(qǐng)求到庫存鎖,并且持有鎖的時(shí)間應(yīng)盡量短。
下面是Java代碼實(shí)現(xiàn):文章來源地址http://www.zghlxwxcb.cn/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 防止惡意刷單
惡意用戶通過程序模擬大量請(qǐng)求,從而導(dǎo)致服務(wù)器無法響應(yīng)正常用戶的請(qǐng)求。為了解決這個(gè)問題,需要加入防止惡意刷單的策略。文章來源:http://www.zghlxwxcb.cn/news/detail-510479.html
實(shí)現(xiàn)方式
- 對(duì)每個(gè)用戶IP進(jìn)行限流,設(shè)置每分鐘能夠請(qǐng)求的次數(shù)。
- 設(shè)置人機(jī)驗(yàn)證,如圖形驗(yàn)證碼或者短信驗(yàn)證等機(jī)制,讓惡意用戶成本太高從而放棄攻擊。
下面是Java代碼實(shí)現(xiàn):
public boolean checkUserRequest(String ip) {
// 檢查ip對(duì)應(yīng)的請(qǐng)求數(shù)是否超過最大允許請(qǐng)求次數(shù)
String requestCountKey = "REQUEST_COUNT_" + ip;
try (Jedis jedis = jedisPool.getResource()) {
long currentCount = jedis.incr(requestCountKey);
if (currentCount == 1) {
// 第一次計(jì)數(shù),設(shè)置過期時(shí)間為60s
jedis.expire(requestCountKey, 60);
}
if (currentCount > maxRequestPerMinute) {
// 超過最大允許請(qǐng)求次數(shù),返回false
return false;
}
return true;
}
}
七、部署方案
1 安全性優(yōu)化
- 部署到專門的CDN緩存服務(wù)器,減小服務(wù)器帶寬壓力,保護(hù)服務(wù)器和數(shù)據(jù)庫。
- 設(shè)置服務(wù)器防火墻,禁止外部訪問 Redis 和 數(shù)據(jù)庫等敏感資源。
- 開啟Redis的持久化,以防止內(nèi)存數(shù)據(jù)丟失或者意外宕機(jī)等情況。
2 性能優(yōu)化
- 提高Redis性能,采用集群方式,增加機(jī)器和配置Redis相關(guān)參數(shù)等。
- 使用高效的緩存查詢方式,避免頻繁查詢數(shù)據(jù)庫,如使用Redis自帶的哈希表來存儲(chǔ)秒殺商品信息。
到了這里,關(guān)于使用 Redis 實(shí)現(xiàn)秒殺系統(tǒng)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!