我的点击计数器实现中存在并发问题。
这是我实现点击计数器的方法。
命中计数器域如下
class PageHitCounter {
String pageIdentifier
int hits
static constraints = {
}
}
在操作中我的代码如下
def verifyRegistration(Long id){
if(springSecurityService.isLoggedIn()){
redirect(controller: "user", action: "index")
return
}
def hitcounter = "${controllerName}/${actionName}/${id}"
def hc = PageHitCounter.findByPageIdentifier(hitcounter)
PageHitCounter.withTransaction {
if(hc){
hc.hits = hc.hits + 1
hc.save()
}
else{
hc = new PageHitCounter(pageIdentifier: hitcounter, hits:1)
hc.save()
}
}
[id: id, hits:hc.hits]
}
hits 变量随后会显示在视图页面中,如下
<div class="hitCounter">
${hits}
</div>
我收到日志的错误是
org.springframework.orm.hibernate5.HibernateOptimisticLockingFailureException: Object of class [com.analytics.PageHitCounter] with identifier [18]: optimistic locking failed; nested exception is org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.analytics.PageHitCounter#18]
at org.springframework.orm.hibernate5.SessionFactoryUtils.convertHibernateAccessException(SessionFactoryUtils.java:284)
at org.springframework.orm.hibernate5.HibernateTransactionManager.convertHibernateAccessException(HibernateTransactionManager.java:802)
at org.springframework.orm.hibernate5.HibernateTransactionManager.doCommit(HibernateTransactionManager.java:638)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:746)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:714)
at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:152)
at grails.gorm.transactions.GrailsTransactionTemplate.execute(GrailsTransactionTemplate.groovy:91)
at org.grails.datastore.gorm.GormStaticApi.with
和
org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.analytics.PageHitCounter#18]
at org.hibernate.persister.entity.AbstractEntityPersister.check(AbstractEntityPersister.java:2604)
at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:3448)
at org.hibernate.persister.entity.AbstractEntityPersister.updateOrInsert(AbstractEntityPersister.java:3311)
at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:3723)
at org.hibernate.action.internal.EntityUpdateAction.execute(EntityUpdateAction.java:201)
at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:604)
at org.hibernate.engine.spi.ActionQueue.lambda$executeActions$1(ActionQueue.java:478)
at java.base/java.util.Li
为什么会触发这个错误是可以理解的。为了识别每个页面,我使用了这个标识符
def hitcounter = "${controllerName}/${actionName}/${id}"
我只是检查具有此标识符的 hitcounter 是否存在,如果不存在,我创建一个,但如果它已经存在,那么我将点击数增加 1。换句话说,第一次访问页面时,将创建 PageHitCounter 对象,之后对象被获取,hits 变量增加 1。因此,如果很多人同时访问该页面,那么我们可以看到,在获取和更新对象时,可能会发生其他更新,这可能会导致乐观锁定失败。
这是我能想到的实现点击计数器功能的唯一解决方案。也许有更好的线程安全方法来实现命中计数器。如果有人可以分享他们发现的在高并发环境中有效的解决方案,我将不胜感激。非常感谢!
当您不在同一事务中同时执行读取和更新操作时,存在另一个线程可能在您读取值和尝试更新它之间修改数据库行的风险。这就是当您收到 HibernateOptimisticLockingFailureException 时会发生的情况。
您应该将从数据库中进行的读取操作转移到事务内部。
此外,您还可以一次性找到或创建点击计数器。
def verifyRegistration(Long id) {
...
def pageIdentifier = "${controllerName}/${actionName}/${id}"
def hitCounter = null
PageHitCounter.withTransaction {
hitCounter = PageHitCounter.findOrCreateByPageIdentifier(pageIdentifier)
hitCounter.hits = hitCounter.hits + 1
hitCounter.save()
}
[id: id, hits: hitCounter?.hits]
}