Slick如何实现记录存在则递增列值、不存在则插入默认值
Slick实现「存在则递增字段、不存在则插入默认记录」逻辑
以下实现针对用户访问计数场景,优先保证并发场景下的数据一致性:
前置准备
无论使用哪种实现方案,必须先给user_id字段加唯一索引,这是判断记录是否存在、避免重复数据的基础,执行对应DDL:
ALTER TABLE user_visit ADD UNIQUE INDEX idx_uv_user_id (user_id);
方案1:数据库原生Upsert语法(推荐,原子性最优)
直接使用数据库自带的INSERT ... ON DUPLICATE KEY UPDATE(MySQL)或INSERT ... ON CONFLICT DO UPDATE(PostgreSQL)语法,单条SQL完成逻辑,天然原子性,高并发下不会出现计数错误,性能最好。
以MySQL为例的实现代码:
import slick.jdbc.MySQLProfile.api._ /** * 递增指定用户的访问次数,不存在则初始化记录 * @param targetUserId 目标用户ID */ def upsertUserVisit(targetUserId: Long): DBIO[Int] = { sqlu""" INSERT INTO user_visit (user_id, visits) VALUES ($targetUserId, 0) ON DUPLICATE KEY UPDATE visits = visits + 1 """ }
如果使用PostgreSQL,只需要替换SQL部分即可:
sqlu""" INSERT INTO user_visit (user_id, visits) VALUES ($targetUserId, 0) ON CONFLICT (user_id) DO UPDATE SET visits = user_visit.visits + 1 """
方案2:纯Slick DSL实现(无原生SQL,需事务包裹)
如果不想编写原生SQL,可以通过「查询→判断存在→更新/插入」的流程实现,必须将整个操作包裹在事务中,否则并发场景下会出现重复插入、计数覆盖的问题。
实现代码:
import slick.jdbc.MySQLProfile.api._ import scala.concurrent.ExecutionContext def incrementUserVisit(targetUserId: Long)(implicit ec: ExecutionContext): DBIO[Long] = { val userQuery = userVisitTable.filter(_.userId === targetUserId) for { existingRecord <- userQuery.result.headOption currentVisits <- existingRecord match { case Some(record) => // 记录存在,直接在数据库层面执行visits+1,避免内存计算导致的覆盖问题 userQuery.map(_.visits).update(record.visits + 1L) .map(_ => record.visits + 1L) case None => // 记录不存在,插入初始值为0的新记录,返回插入后的visits值 (userVisitTable returning userVisitTable.map(_.visits)) += UserVisitFO(userId = targetUserId, visits = 0L) } } yield currentVisits }.transactionally // 必须加事务,保证查询和写入的原子性
注意事项
- 禁止在无事务的情况下先查询再写入:并发场景下多个请求同时查到记录不存在,会触发重复插入、唯一键冲突问题
- 递增操作必须在SQL层面执行
visits = visits + 1,不要把字段值查到内存里加1之后再update,否则高并发下会出现更新覆盖,导致计数不准 - 生产环境优先选择方案1的原生Upsert实现,性能和一致性都优于DSL分步实现
内容的提问来源于stack exchange,提问作者Alireza
相关产品推荐
相关产品推荐

