You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Grails中实现线程安全的页面访问计数器?

高并发下实现线程安全的页面访问计数器(Grails环境)

你的问题核心是先查询再更新的竞态条件:高并发时多个事务同时读取到相同的hits值,各自加1后提交,导致乐观锁检测到版本冲突抛出异常。下面提供三种可行的解决方案:

方案1:使用悲观锁(Pessimistic Locking)

在查询计数器时直接加行级锁,确保同一时间只有一个事务能修改这条记录,从根源避免竞态。

修改控制器中的查询逻辑,给查询添加lock: true参数:

def verifyRegistration(Long id){
    if(springSecurityService.isLoggedIn()){
        redirect(controller: "user", action: "index")
        return
    }

    def hitcounter = "${controllerName}/${actionName}/${id}"

    PageHitCounter.withTransaction {
        // 查询时加悲观锁
        def hc = PageHitCounter.findByPageIdentifier(hitcounter, [lock: true])
        if(hc){
            hc.hits += 1
            hc.save(failOnError: true)
        } else {
            hc = new PageHitCounter(pageIdentifier: hitcounter, hits:1)
            hc.save(failOnError: true)
        }
    }

    [id: id, hits: hc.hits]
}

优点:逻辑简单,无需额外处理异常;缺点:高并发下锁竞争会导致部分请求等待,吞吐量略有下降,适合计数器更新频率不是极端高的场景。

方案2:数据库原子更新(推荐)

跳过实体加载,直接用SQL执行原子性的递增操作,利用数据库的原生原子性保证线程安全,性能最优。

修改控制器代码,使用executeUpdate先尝试更新,若更新行数为0则创建新记录:

def verifyRegistration(Long id){
    if(springSecurityService.isLoggedIn()){
        redirect(controller: "user", action: "index")
        return
    }

    def hitcounter = "${controllerName}/${actionName}/${id}"
    int updatedRows

    PageHitCounter.withTransaction {
        // 原子递增,返回更新行数
        updatedRows = PageHitCounter.executeUpdate(
            "UPDATE PageHitCounter SET hits = hits + 1 WHERE pageIdentifier = :pid",
            [pid: hitcounter]
        )
        // 如果没有更新到记录,说明是首次访问,创建新计数器
        if(updatedRows == 0){
            def hc = new PageHitCounter(pageIdentifier: hitcounter, hits:1)
            hc.save(failOnError: true)
        }
    }

    // 最后查询最新的计数
    def hc = PageHitCounter.findByPageIdentifier(hitcounter)
    [id: id, hits: hc?.hits ?: 1]
}

优点:完全利用数据库原子操作,无锁竞争问题,性能最高;缺点:需要手写JPQL/SQL,对ORM的依赖降低,但逻辑依然清晰。

方案3:乐观锁+重试机制

保留乐观锁(给实体添加版本字段),当出现乐观锁异常时自动重试更新操作,适合需要保留ORM操作习惯的场景。

首先修改PageHitCounter域类,添加乐观锁版本字段:

class PageHitCounter {
    String pageIdentifier
    int hits
    int version // 乐观锁字段,Grails会自动维护版本号

    static constraints = {
    }
}

然后修改控制器代码,捕获乐观锁异常并重试:

def verifyRegistration(Long id){
    if(springSecurityService.isLoggedIn()){
        redirect(controller: "user", action: "index")
        return
    }

    def hitcounter = "${controllerName}/${actionName}/${id}"
    int retryCount = 3 // 设置最大重试次数
    def hc = null

    while(retryCount-- > 0){
        try {
            PageHitCounter.withTransaction {
                hc = PageHitCounter.findByPageIdentifier(hitcounter)
                if(hc){
                    hc.hits += 1
                    hc.save(failOnError: true)
                } else {
                    hc = new PageHitCounter(pageIdentifier: hitcounter, hits:1)
                    hc.save(failOnError: true)
                }
            }
            break // 成功更新,跳出循环
        } catch(org.springframework.orm.hibernate5.HibernateOptimisticLockingFailureException e){
            // 乐观锁异常,继续重试
            continue
        }
    }

    [id: id, hits: hc?.hits ?: 0]
}

优点:无需修改数据库锁机制,适合低冲突场景;缺点:高冲突时会有重试开销,需要合理设置重试次数。


内容的提问来源于stack exchange,提问作者kofhearts

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 01:34:53