从零构建高可用skill商店:新手避坑指南与架构设计实战

3次阅读
没有评论

共计 3670 个字符,预计需要花费 10 分钟才能阅读完成。

image.webp

背景痛点分析

开发 skill 商店时,新手常会遇到几个典型问题:

从零构建高可用 skill 商店:新手避坑指南与架构设计实战

  • 商品状态同步延迟 :当多个用户同时购买同一技能时,库存状态更新可能不同步
  • 技能权限控制缺失 :未购买用户可能通过接口直接访问付费技能内容
  • 接口设计混乱 :返回数据结构不统一,导致前端处理逻辑复杂
  • 高并发查询压力 :热门技能页面访问时数据库负载激增

这些问题往往在流量增长后集中爆发,因此前期架构设计尤为关键。

技术选型对比

Firebase 方案

  • 优点
  • 开箱即用的身份验证和实时数据库
  • 无需维护服务器基础设施
  • 快速原型开发

  • 缺点

  • vendor lock-in 风险
  • 复杂查询性能较差
  • 成本随用户量非线性增长

自建 Node.js+MongoDB 方案

  • 决策依据
  • MongoDB 的文档模型天然适合技能商品的多变属性
  • Node.js 非阻塞 IO 适合高并发 I / O 密集型场景
  • 完整的架构控制权
  • 成本可预测性强

核心实现

商品 Schema 设计

interface ISkill {
  title: string;
  description: string;
  category: 'programming' | 'design' | 'language';
  pricingModel: {
    type: 'one-time' | 'subscription';
    price: number;
    duration?: number; // 针对订阅类型
  };
  inventory: {
    total: number;
    available: number;
    lock?: number; // 预占库存
  };
  requirements: string[];
  createdAt: Date;
  updatedAt: Date;
}

const skillSchema = new mongoose.Schema<ISkill>({// ... 字段定义}, {timestamps: true});

JWT 鉴权实现

// auth.middleware.ts
export const authenticate = async (req: Request, res: Response, next: NextFunction) => {
  try {const token = req.header('Authorization')?.replace('Bearer', '');
    if (!token) throw new Error('Authentication required');

    const decoded = jwt.verify(token, process.env.JWT_SECRET!) as {_id: string};
    const user = await User.findById(decoded._id).select('-password');

    if (!user) throw new Error('User not found');

    req.user = user;
    next();} catch (error) {if (error instanceof jwt.TokenExpiredError) {return res.status(401).json({ 
        code: 'TOKEN_EXPIRED',
        message: 'Token expired',
        refreshUrl: '/auth/refresh'
      });
    }
    res.status(401).json({error: 'Authentication failed'});
  }
};

// 刷新令牌接口
router.post('/refresh', async (req: Request, res: Response) => {const { refreshToken} = req.body;
  // ... 验证逻辑
  const newAccessToken = generateToken(user._id, '15m');
  res.json({accessToken: newAccessToken});
});

性能优化策略

Redis 缓存设计

// 获取技能详情带缓存
async function getSkillWithCache(skillId: string) {const cacheKey = `skill:${skillId}`;

  try {
    // 先查缓存
    const cachedData = await redisClient.get(cacheKey);
    if (cachedData) return JSON.parse(cachedData);

    // 缓存未命中,查数据库
    const skill = await Skill.findById(skillId).lean();
    if (!skill) return null;

    // 设置缓存(TTL 30 分钟)await redisClient.setEx(cacheKey, 1800, JSON.stringify(skill));

    return skill;
  } catch (error) {logger.error('Cache operation failed', { skillId, error});
    // 降级直接查数据库
    return await Skill.findById(skillId).lean();}
}

// 防缓存穿透
async function getSkillSafe(skillId: string) {if (!isValidObjectId(skillId)) return null;

  const skill = await getSkillWithCache(skillId);
  if (!skill) {
    // 设置空值缓存防止穿透
    await redisClient.setEx(`skill:${skillId}`, 60, 'null');
  }
  return skill;
}

分片集群部署

建议采用 3 分片集群:

  1. 按技能类别哈希分片
  2. 配置副本集确保高可用
  3. 查询路由通过 mongos 实现

避坑指南

事务处理要点

// 购买技能的事务处理
async function purchaseSkill(userId: string, skillId: string) {const session = await mongoose.startSession();
  session.startTransaction();

  try {
    // 1. 检查技能可用性
    const skill = await Skill.findById(skillId).session(session);
    if (skill.inventory.available <= 0) {throw new Error('Out of stock');
    }

    // 2. 扣减库存(使用原子操作)const updated = await Skill.updateOne({ _id: skillId, 'inventory.available': { $gt: 0} },
      {$inc: { 'inventory.available': -1} }
    ).session(session);

    if (updated.modifiedCount === 0) {throw new Error('Concurrent modification');
    }

    // 3. 创建订单
    const order = new Order({userId, skillId});
    await order.save({session});

    // 4. 提交事务
    await session.commitTransaction();

    // 5. 清除相关缓存
    await redisClient.del(`skill:${skillId}`);

    return order;
  } catch (error) {await session.abortTransaction();
    logger.error('Purchase failed', { userId, skillId, error});
    throw error;
  } finally {session.endSession();
  }
}

技能交付幂等性

// 技能交付接口(幂等实现)router.post('/deliver', async (req: Request, res: Response) => {const { orderId, nonce} = req.body;

  // 检查是否已处理过该请求
  const processed = await redisClient.get(`delivery:${nonce}`);
  if (processed) {return res.json({ status: 'already_delivered'});
  }

  // 标记请求已处理(24 小时过期)await redisClient.setEx(`delivery:${nonce}`, 86400, '1');

  // 实际交付逻辑...
});

总结

构建高可用 skill 商店需要特别注意数据一致性和系统扩展性。通过合理的架构设计:

  1. 数据层 :MongoDB 分片集群处理增长
  2. 缓存层 :Redis 减轻数据库压力
  3. 应用层
  4. JWT 实现无状态认证
  5. 事务保证数据一致性
  6. 幂等设计避免重复操作
  7. 监控 :关键操作添加日志埋点

建议在开发初期就建立性能基准测试,随着用户增长逐步引入:
– CDN 加速静态资源
– 消息队列处理异步任务
– 服务网格管理微服务

这套架构已支撑我们平台日均 10 万 + 交易,希望这些实践对你有帮助。

正文完
 0
评论(没有评论)