积分变更必须用事务包裹,先插入含type/related_id/remark等字段的流水,再更新用户积分,并用select...for update防止并发覆盖。

积分增减必须用事务包裹,否则数据对不上
用户做任务得 10 分、消费扣 5 分,如果中间出错(比如写完积分没写流水),就会出现“账户多了分但查不到来源”的情况。MySQL 的 START TRANSACTION 是底线,不是可选项。
常见错误现象:UPDATE users SET score = score + 10 WHERE id = 123 单独执行后,INSERT INTO score_logs 因字段缺失或唯一键冲突失败,导致积分已加、流水没留。
- 所有积分变更操作(含奖励、扣除、清零)必须在同一个事务里完成:先
INSERT流水,再UPDATE用户积分 - 流水表必须包含
score_before和score_after字段,便于事后核对和回滚 - 不要用
score = score + ?计算新值,而应先SELECT score FROM users WHERE id = ? FOR UPDATE,再算出score_after,避免并发覆盖
流水表设计要能区分来源类型,别只存 amount
光记“+10”没用,下次运营问“为什么张三昨天突然涨了 100 分”,你得立刻答出是签到、邀请、还是客服手动补的。字段设计直接影响排查效率。
使用场景:后台查异常、导出用户成长路径、配合风控判断刷分行为。
- 必加字段:
type(如'sign_in'、'invite_friend'、'admin_adjust')、related_id(关联任务 ID 或管理员 ID)、remark(非空字符串,哪怕只是'system auto') - 避免用整数枚举 type:后期加类型要改库,直接用短字符串更灵活
-
created_at必须用NOW(3)或应用层传入精确到毫秒的时间,别依赖 PHPtime()再转格式,时区和精度容易错
并发扣分时,用 SELECT ... FOR UPDATE 比乐观锁更稳
用户余额不足还点兑换,两个请求同时读到 “当前 8 分,要扣 10 分”,都判断成功然后都去扣——结果变成 -2 分。乐观锁靠 version 字段重试,在高并发下失败率高、体验差。
性能影响:FOR UPDATE 会锁住对应行,但只要事务够短(不混杂 HTTP 请求、日志、外部 API 调用),实际压测中 QPS 300+ 也没问题。
- 扣分前必须先
SELECT score FROM users WHERE id = ? FOR UPDATE,再判断是否足够 - 不能把
FOR UPDATE放在事务开头就不管了——它只锁住本次 SELECT 查到的行,后续 UPDATE 不会自动续锁 - 如果业务允许“透支”,那也要在事务内原子化更新
score和overdraft_used字段,不能分开处理
别在流水里存用户昵称或头像,关联查就行
有人图省事,在 score_logs 表里直接存 nike_name 字段,结果用户改名了,历史流水全显示旧名字,运营一查就懵。
兼容性影响:用户表加字段、改字符集、迁移分库时,流水表跟着一起动,耦合度飙升。
- 流水表只存
user_id,展示时 JOINusers表取最新昵称 - 如果担心 JOIN 影响查询性能(比如导出百万级流水),可以加覆盖索引:
INDEX(user_id, created_at) - 绝对不要在流水表里冗余存
avatar_url、level这类动态字段——它们变一次,你就得扫一遍历史流水
最常被忽略的是事务边界和流水字段语义。很多人测试时单线程跑通就上线,等大促一来,积分对不上、来源查不清、用户投诉一堆,才想起翻日志——但流水里没 type 和 related_id,根本没法定位。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











