如何在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
相关产品推荐
相关产品推荐

