电商秒杀系统崩溃怎么办MySQL高并发下的读写分离分库分表与缓存优化实战方案
那个凌晨三点被电话叫醒的夜晚
2023年双11凌晨,我的手机响了。是运维老张,声音都在抖:”服务器扛不住了,秒杀接口全在报500”。屏幕上一片红,QPS从平时的几千瞬间飙到八万多,MySQL连接池直接爆掉,缓存命中率跌破10%。那一刻,我站在公司落地窗前,看着凌晨的北京,心里只有一个念头:这仗,得重新打。
今天这篇,就是把那次教训全部复盘出来,从读写分离到分库分表,再到缓存架构,每一条都是血泪换来的实战经验。
一、先搞清楚,秒杀到底在跟谁作战
很多人以为秒杀就是”很多人同时买东西”,太简单化了。你面对的是三座大山:
量级的暴力。平时每秒几百个请求,秒杀时是几十万的瞬时峰值,数据库的连接数、CPU、内存,哪个都不是线性增长,而是指数级爆炸。
时间的残酷。秒杀就那一秒、两秒,系统必须在这个窗口内扛住,慢一秒,排队的人就崩了。
一致的悖论。用户看到”抢购成功”,但库存不能超卖,不能少卖,这在分布式环境下是个经典难题。
所以,你的架构必须像特种部队一样,平时隐蔽,战时精准。下面这些方案,是我在无数次压测和故障中打磨出来的。
二、读写分离:别把所有压力都给一个人扛
为什么需要读写分离
想象一下,你的MySQL主库既要写(库存扣减、订单生成),又要读(商品详情、活动信息),结果就是:写的地方排队,读的地方也堵死。主库负载过高,直接拖垮整个系统。
读写分离的本质,是把读操作分流到从库,让主库专心写。
架构设计
┌──────────────┐
│ 网关/负载均衡 │
└──────┬───────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 业务读库1 │ │ 业务读库2 │ │ 业务读库3 │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
└────────────┴────────────┘
│
▼
┌──────────┐
│ MySQL主库 │ ← 只负责写操作
│ (Master) │
└──────────┘
│
异步复制
│
┌─────┴─────┐
▼ ▼
从库1 从库2
实战代码:MyBatis读写分离配置
<!-- druid连接池配置,读写分离核心 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/seckill_db"/>
<property name="username" value="root"/>
<property name="password" value="password"/>
<!-- 最大连接数 -->
<property name="maxActive" value="200"/>
<!-- 最小连接数 -->
<property name="minIdle" value="10"/>
<!-- 连接等待超时时间 -->
<property name="maxWait" value="3000"/>
</bean>
<!-- 动态数据源,实现读写分离 -->
<bean id="dynamicDataSource" class="com.yourcompany.datasource.DynamicDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<!-- 写数据源 -->
<entry key="write" value-ref="writeDataSource"/>
<!-- 读数据源 -->
<entry key="read1" value-ref="readDataSource1"/>
<entry key="read2" value-ref="readDataSource2"/>
</map>
</property>
<!-- 默认数据源 -->
<property name="defaultTargetDataSource" ref="writeDataSource"/>
</bean>
<!-- 写数据源 -->
<bean id="writeDataSource" parent="dataSource">
<property name="url" value="jdbc:mysql://master:3306/seckill_db"/>
</bean>
<!-- 读数据源1 -->
<bean id="readDataSource1" parent="dataSource">
<property name="url" value="jdbc:mysql://slave1:3306/seckill_db"/>
</bean>
<!-- 读数据源2 -->
<bean id="readDataSource2" parent="dataSource">
<property name="url" value="jdbc:mysql://slave2:3306/seckill_db"/>
</bean>
// 动态数据源切换逻辑
public class DynamicDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
/**
* 决定使用哪个数据源
*/
@Override
protected Object determineCurrentLookupKey() {
return getDataSourceType();
}
/**
* 设置数据源类型
*/
public static void setDataSourceType(String type) {
CONTEXT_HOLDER.set(type);
}
/**
* 获取数据源类型
*/
public static String getDataSourceType() {
String type = CONTEXT_HOLDER.get();
return type != null ? type : "read"; // 默认走读库
}
/**
* 清除数据源类型(用完必须清除,否则线程池复用会出问题)
*/
public static void clearDataSourceType() {
CONTEXT_HOLDER.remove();
}
}
// AOP切面,自动根据方法名判断读写
@Aspect
@Component
public class DataSourceAspect {
@Before("@annotation(readOnly)")
public void setReadDataSource(JoinPoint point, ReadOnly readOnly) {
DynamicDataSource.setDataSourceType("read");
}
@Before("@annotation(writeOnly)")
public void setWriteDataSource(JoinPoint point, WriteOnly writeOnly) {
DynamicDataSource.setDataSourceType("write");
}
@AfterReturning
public void clearDataSource(JoinPoint point) {
DynamicDataSource.clearDataSourceType();
}
}
// 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnly {}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface WriteOnly {}
// 服务层使用示例
@Service
public class SeckillService {
@WriteOnly
public SeckillOrder createOrder(Long userId, Long productId) {
// 扣减库存、生成订单 - 强制走写库
// ...
}
@ReadOnly
public SeckillProduct getProductInfo(Long productId) {
// 查询商品详情 - 走读库
// ...
}
}
读写分离的几个坑
主从延迟。这是读写分离最大的敌人。如果从库还没同步完主库的数据,用户刚下单就去查订单,可能查不到。解决方案:
// 关键写操作后,强制读主库
public SeckillOrder createOrderAndQuery(Long userId, Long productId) {
// 1. 写操作
SeckillOrder order = seckillMapper.insertOrder(userId, productId);
// 2. 写完后,强制读主库,避免查到旧数据
DynamicDataSource.setDataSourceType("write");
try {
// 3. 查询刚才插入的订单
return seckillMapper.selectById(order.getId());
} finally {
DynamicDataSource.clearDataSourceType();
}
}
读库故障。多搞几个读库,负载均衡,一个挂了自动切换。
三、分库分表:解决单库性能瓶颈的终极武器
为什么要分库分表
一个MySQL实例,哪怕配置再高,能扛的QPS也有上限。当单表数据超过千万级,查询性能断崖式下跌。分库分表就是把一个大库拆成多个小库,一个大表拆成多个小表,让每个实例扛的负载可控。
分库策略
常见的分库方式有几种:
| 方式 | 优点 | 缺点 |
|---|---|---|
| 按用户ID取模 | 均匀分布,扩展方便 | 跨库查询复杂 |
| 按地区分 | 本地化访问快 | 地区分布不均 |
| 按时间分 | 历史数据好管理 | 新数据集中 |
秒杀系统建议用用户ID取模,因为每个用户的请求量相对均匀。
/**
* 分库分表工具类
*/
public class ShardingUtils {
/**
* 根据用户ID计算库索引
*/
public static int getDbIndex(Long userId, int dbCount) {
return (int) (Math.abs(userId) % dbCount);
}
/**
* 根据订单ID计算表索引
*/
public static int getTableIndex(Long orderId, int tableCount) {
return (int) (Math.abs(orderId) % tableCount);
}
}
分表策略
以订单表为例,假设分16张表:
-- 原始表
CREATE TABLE seckill_order (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
order_status TINYINT DEFAULT 0,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_id (user_id),
INDEX idx_create_time (create_time)
);
-- 分表后(16张表)
CREATE TABLE seckill_order_00 (LIKE seckill_order);
CREATE TABLE seckill_order_01 (LIKE seckill_order);
-- ... 到 seckill_order_15
实战:动态SQL路由
@Repository
public class SeckillOrderMapper {
@Autowired
private DataSource dynamicDataSource;
/**
* 插入订单,自动路由到正确的库和表
*/
@WriteOnly
public long insertOrder(SeckillOrder order) {
// 1. 确定库
int dbIndex = ShardingUtils.getDbIndex(order.getUserId(), 4); // 4个库
DataSource db = getDataSourceByIndex(dbIndex);
// 2. 确定表
long orderId = generateOrderId(); // 用雪花算法生成全局唯一ID
int tableIndex = ShardingUtils.getTableIndex(orderId, 16); // 16张表
String tableName = "seckill_order_" + String.format("%02d", tableIndex);
// 3. 切换到对应数据源执行
DynamicDataSource.setDataSourceType("write_" + dbIndex);
try {
String sql = "INSERT INTO " + tableName
+ " (id, user_id, product_id, order_status, create_time) "
+ "VALUES (#id#, #userId#, #productId#, #orderStatus#, #createTime#)";
KeyGeneratedKeyHolder keyHolder = new KeyGeneratedKeyHolder();
jdbcTemplate.update(connection -> {
PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
ps.setLong(1, orderId);
ps.setLong(2, order.getUserId());
ps.setLong(3, order.getProductId());
ps.setInt(4, order.getOrderStatus());
ps.setTimestamp(5, new Timestamp(order.getCreateTime().getTime()));
return ps;
}, keyHolder);
return (long) keyHolder.getKey();
} finally {
DynamicDataSource.clearDataSourceType();
}
}
}
分布式ID生成:雪花算法
分库分表后,不能再用自增ID,需要全局唯一ID:
public class SnowflakeIdGenerator {
/** 起始时间戳 (2020-01-01) */
private static final long EPOCH = 1577836800000L;
/** 机器ID所占位数 */
private static final long WORKER_ID_BITS = 5L;
/** 数据中心ID所占位数 */
private static final long DATA_CENTER_ID_BITS = 5L;
/** 序列号所占位数 */
private static final long SEQUENCE_BITS = 12L;
/** 机器ID最大值 */
private static final long MAX_WORKER_ID = -1L ^ (-1L << WORKER_ID_BITS);
/** 数据中心ID最大值 */
private static final long MAX_DATA_CENTER_ID = -1L ^ (-1L << DATA_CENTER_ID_BITS);
/** 机器ID左移位数 */
private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
/** 数据中心ID左移位数 */
private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
/** 时间戳左移位数 */
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS;
/** 序列号掩码 */
private static final long SEQUENCE_MASK = -1L ^ (-1L << SEQUENCE_BITS);
private long workerId;
private long dataCenterId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(long workerId, long dataCenterId) {
if (workerId > MAX_WORKER_ID || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", MAX_WORKER_ID));
}
if (dataCenterId > MAX_DATA_CENTER_ID || dataCenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", MAX_DATA_CENTER_ID));
}
this.workerId = workerId;
this.dataCenterId = dataCenterId;
}
/**
* 生成下一个ID
*/
public synchronized long nextId() {
long timestamp = timeGen();
// 时钟回拨检测
if (timestamp < lastTimestamp) {
// 容忍5毫秒内的回拨,超过则抛出异常
long offset = lastTimestamp - timestamp;
if (offset <= 5) {
try {
waitForMillis(offset);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
throw new IllegalStateException("Clock moved backwards. Refusing to generate id for " + offset + " milliseconds");
}
timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new IllegalStateException("Clock moved backwards. Refusing to generate id");
}
}
// 同一毫秒内,序列号自增
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & SEQUENCE_MASK;
// 序列号溢出,等下一毫秒
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
// 不同毫秒,序列号重置
sequence = 0L;
}
lastTimestamp = timestamp;
// 组装ID
return ((timestamp - EPOCH) << TIMESTAMP_SHIFT)
| (dataCenterId << DATA_CENTER_ID_SHIFT)
| (workerId << WORKER_ID_SHIFT)
| sequence;
}
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
private void waitForMillis(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private long timeGen() {
return System.currentTimeMillis();
}
}
四、缓存优化:秒杀系统的最后一道防线
为什么缓存是必须滴
没有缓存的秒杀系统,每一次请求都要打穿到数据库,这跟裸奔没什么区别。缓存是秒杀系统的”缓冲垫”,把大部分请求挡在数据库外面。
缓存架构设计
用户请求
│
▼
┌─────────────────┐
│ CDN/边缘缓存 │ ← 静态资源、商品图片
└────────┬────────┘
│
┌────▼────┐
│ Redis │ ← 热点数据、库存扣减
│ (集群) │
└────┬────┘
│ 缓存未命中
┌────▼────┐
│ MySQL │ ← 持久化存储
└─────────┘
Redis缓存策略
策略一:Cache Aside Pattern(旁路缓存)
@Service
public class ProductService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private SeckillProductMapper productMapper;
/**
* 查询商品,先查缓存,缓存没有再查数据库
*/
public SeckillProduct getProduct(Long productId) {
// 1. 查缓存
String cacheKey = "seckill:product:" + productId;
String cacheValue = redisTemplate.opsForValue().get(cacheKey);
if (cacheValue != null) {
// 缓存命中,直接返回
return JSON.parseObject(cacheValue, SeckillProduct.class);
}
// 2. 缓存未命中,查数据库
SeckillProduct product = productMapper.selectById(productId);
if (product == null) {
// 防止缓存穿透,空值也缓存
redisTemplate.opsForValue().set(cacheKey, "NULL", 30, TimeUnit.SECONDS);
return null;
}
// 3. 写入缓存
redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(product), 30, TimeUnit.SECONDS);
return product;
}
/**
* 更新商品,先更新数据库,再删除缓存
*/
public void updateProduct(SeckillProduct product) {
productMapper.updateById(product);
// 删除缓存,下次查询会重新加载
String cacheKey = "seckill:product:" + product.getId();
redisTemplate.delete(cacheKey);
}
}
策略二:秒杀库存的缓存扣减
这是最关键的部分。秒杀的核心是库存扣减,如果用数据库来扣,扛不住高并发。
@Service
public class SeckillService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private SeckillOrderMapper orderMapper;
/**
* 秒杀扣减库存
* 使用Redis原子操作,避免超卖
*/
@WriteOnly
public SeckillResult seckill(Long userId, Long productId) {
String stockKey = "seckill:stock:" + productId;
String userKey = "seckill:user:" + productId + ":" + userId;
// 1. 检查用户是否已抢购(防重复购买)
Boolean hasBought = redisTemplate.opsForSet().isMember(userKey, String.valueOf(userId));
if (Boolean.TRUE.equals(hasBought)) {
return SeckillResult.fail("您已抢购过该商品");
}
// 2. 原子扣减库存
Long remainingStock = redisTemplate.opsForValue().decrement(stockKey);
if (remainingStock == null || remainingStock < 0) {
// 库存不足,恢复计数
redisTemplate.opsForValue().increment(stockKey);
return SeckillResult.fail("库存不足");
}
// 3. 记录用户已购买
redisTemplate.opsForSet().add(userKey, String.valueOf(userId));
redisTemplate.expire(userKey, 24, TimeUnit.HOURS);
// 4. 异步写入数据库(MQ)
SeckillMessage message = new SeckillMessage(userId, productId);
kafkaTemplate.send("seckill-order", JSON.toJSONString(message));
return SeckillResult.success("抢购成功,请等待订单生成");
}
/**
* 预扣库存(活动开始前预热)
*/
public void preLoadStock(Long productId, int totalStock) {
String stockKey = "seckill:stock:" + productId;
// 设置初始库存
redisTemplate.opsForValue().set(stockKey, String.valueOf(totalStock));
// 设置过期时间,防止活动后数据残留
redisTemplate.expire(stockKey, 7, TimeUnit.DAYS);
}
}
/**
* 库存扣减的Lua脚本(保证原子性)
*/
public class SeckillLuaScript {
private static final String LUA_SCRIPT =
"local stock = redis.call('GET', KEYS[1]) " +
"if not stock then " +
" return -2 " +
"end " +
"if tonumber(stock) <= 0 then " +
" return -1 " +
"end " +
"redis.call('DECR', KEYS[1]) " +
"return 1";
/**
* 原子扣减库存,返回结果:1成功,-1库存不足,-2不存在
*/
public static Long decrementStock(RedisTemplate<String, String> redisTemplate, String stockKey) {
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptText(LUA_SCRIPT);
script.setResultType(Long.class);
return redisTemplate.execute(script, Collections.singletonList(stockKey));
}
}
缓存穿透、击穿、雪崩的解决方案
穿透:查询不存在的数据,每次都打到数据库。 解决方案:缓存空值。
击穿:热点Key过期,大量请求同时打到数据库。 解决方案:永不过期 + 逻辑过期。
/**
* 逻辑过期方案,避免缓存击穿
*/
@Service
public class HotProductCacheService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private SeckillProductMapper productMapper;
/**
* 查询热点商品,使用逻辑过期
*/
public SeckillProduct getHotProduct(Long productId) {
String cacheKey = "hot:product:" + productId;
// 1. 获取缓存
String cacheJson = redisTemplate.opsForValue().get(cacheKey);
if (cacheJson == null) {
// 缓存不存在,直接查数据库并写入缓存
return loadFromDb(productId);
}
// 2. 解析缓存,检查是否过期
CacheEntry entry = JSON.parseObject(cacheJson, CacheEntry.class);
if (entry.isExpire()) {
// 已过期,异步刷新缓存,当前请求继续使用旧数据
asyncRefreshCache(productId, entry);
return entry.getProduct();
}
// 3. 缓存有效,直接返回
return entry.getProduct();
}
/**
* 异步刷新缓存
*/
private void asyncRefreshCache(Long productId, CacheEntry entry) {
CompletableFuture.runAsync(() -> {
try {
SeckillProduct product = loadFromDb(productId);
CacheEntry newEntry = new CacheEntry(product, System.currentTimeMillis() + 60000);
redisTemplate.opsForValue().set(
"hot:product:" + productId,
JSON.toJSONString(newEntry),
70, TimeUnit.SECONDS
);
} catch (Exception e) {
log.error("刷新缓存失败", e);
}
});
}
private SeckillProduct loadFromDb(Long productId) {
SeckillProduct product = productMapper.selectById(productId);
if (product != null) {
CacheEntry entry = new CacheEntry(product, System.currentTimeMillis() + 60000);
redisTemplate.opsForValue().set(
"hot:product:" + productId,
JSON.toJSONString(entry),
70, TimeUnit.SECONDS
);
}
return product;
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CacheEntry {
private SeckillProduct product;
private long expireTime; // 逻辑过期时间戳
public boolean isExpire() {
return System.currentTimeMillis() > expireTime;
}
}
雪崩:大量缓存同时过期。 解决方案:过期时间加随机值。
// 设置过期时间时,加一个随机值
long expireSeconds = 300 + new Random().nextInt(60); // 300-360秒之间随机
redisTemplate.opsForValue().set(key, value, expireSeconds, TimeUnit.SECONDS);
五、完整的秒杀请求流程
把上面所有内容串起来,一个完整的秒杀流程是这样的:
用户点击"抢购"
│
▼
┌─────────────────────────────────────┐
│ 1. 网关层:限流、鉴权 │
│ - 每个用户每秒最多1次请求 │
│ - 非法请求直接返回 │
└──────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 2. 缓存层:Redis预扣库存 │
│ - 检查是否已购买(Set) │
│ - 原子扣减库存(DECR/Lua) │
│ - 库存不足直接返回 │
└──────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 3. 消息队列:异步下单 │
│ - 发送MQ消息 │
│ - 立即返回"排队中" │
└──────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 4. 消费者:异步写入数据库 │
│ - 接收MQ消息 │
│ - 分库分表写入订单 │
│ - 更新库存(最终一致性) │
└──────────────┬──────────────────────┘
│
▼
用户查询订单结果
网关限流代码
@RestController
@RequestMapping("/api/seckill")
public class SeckillController {
@Autowired
private SeckillService seckillService;
@Autowired
private RateLimiter rateLimiter;
/**
* 秒杀接口
*/
@PostMapping("/seckill/{productId}")
public ResponseEntity<SeckillResult> seckill(
@RequestHeader("UserId") Long userId,
@PathVariable Long productId) {
// 1. 限流检查
if (!rateLimiter.tryAcquire(userId)) {
return ResponseEntity.ok(SeckillResult.fail("请求过于频繁,请稍后重试"));
}
// 2. 执行秒杀
SeckillResult result = seckillService.seckill(userId, productId);
return ResponseEntity.ok(result);
}
}
@Component
public class RateLimiter {
// 每个用户每秒最多1次请求
private final ConcurrentHashMap<Long, Long> requestCounts = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, Long> windowStartTimes = new ConcurrentHashMap<>();
/**
* 尝试获取许可
*/
public boolean tryAcquire(Long userId) {
long now = System.currentTimeMillis();
long windowStart = now / 1000 * 1000; // 当前秒的起始时间
// 检查是否是新的时间窗口
Long lastWindowStart = windowStartTimes.get(userId);
if (lastWindowStart == null || lastWindowStart < windowStart) {
// 新窗口,重置计数
requestCounts.put(userId, 1L);
windowStartTimes.put(userId, windowStart);
return true;
}
// 同一窗口内,检查计数
long count = requestCounts.getOrDefault(userId, 0L);
if (count >= 1) {
return false; // 超出限流
}
requestCounts.put(userId, count + 1);
return true;
}
}
六、那些你没想到的细节
1. 数据库连接池调优
秒杀场景下,连接池的参数必须仔细调优:
spring:
datasource:
hikari:
maximum-pool-size: 50 # 最大连接数,根据压测结果调整
minimum-idle: 10 # 最小空闲连接
idle-timeout: 30000 # 空闲连接超时
max-lifetime: 600000 # 连接最大生命周期
connection-timeout: 3000 # 获取连接超时
connection-test-query: SELECT 1
2. 索引优化
-- 订单表索引
CREATE INDEX idx_user_product ON seckill_order (user_id, product_id);
CREATE INDEX idx_status_time ON seckill_order (order_status, create_time);
-- 库存表索引
CREATE INDEX idx_product_stock ON seckill_stock (product_id, stock);
3. 数据库参数调优
# my.cnf 关键参数
innodb_buffer_pool_size = 8G # 缓存池大小,设置为内存的50-70%
innodb_log_file_size = 512M # 日志文件大小
innodb_flush_log_at_trx_commit = 2 # 每秒刷盘,提升写性能(秒杀场景可容忍)
max_connections = 1000 # 最大连接数
slow_query_log = 1 # 开启慢查询日志
long_query_time = 0.1 # 超过100ms算慢查询
七、如果还是崩了怎么办
任何系统都有上限,当压力超出设计容量时,你需要”保命”策略:
1. 熔断降级
/**
* 熔断器配置
*/
@CircuitBreaker(name = "seckillService", fallbackMethod = "seckillFallback")
public SeckillResult seckill(Long userId, Long productId) {
// 正常逻辑
}
/**
* 降级逻辑
*/
public SeckillResult seckillFallback(Long userId, Long productId, Exception e) {
log.error("秒杀服务异常,降级处理", e);
return SeckillResult.fail("系统繁忙,请稍后重试");
}
2. 动态开关
@Component
public class SeckillSwitch {
@Value("${seckill.switch:on}")
private boolean switchOn;
public boolean isSwitchOn() {
return switchOn;
}
/**
* 手动关闭开关(通过配置中心动态下发)
*/
public void setSwitchOn(boolean on) {
this.switchOn = on;
}
}
// 使用开关
if (!seckillSwitch.isSwitchOn()) {
return SeckillResult.fail("活动已结束");
}
3. 排队系统
当Redis库存扣减成功后,如果数据库写不进去,不要让用户一直等,给他们一个”排队中”的状态,然后异步处理。
八、最后说几句
那次双11之后,我们团队复盘了整整一周。发现最大的问题不是技术,而是预案不足。我们没有考虑到流量洪峰的形状——不是一直高,而是突然一个尖峰。
后来我们做了几件事:
- 全链路压测。上线前模拟真实流量,找出瓶颈。
- 混沌工程。随机杀掉服务实例,检验系统的容错能力。
- 灰度发布。新功能先上10%的流量,观察无误再全量。
技术方案的优化是无止境的。读写分离、分库分表、缓存优化,这些只是基础。真正的核心是:了解你的业务,预测你的流量,做好最坏的打算。
希望这篇能帮到你。如果有任何问题,欢迎讨论。
