select for update [nowait | wait n] support Oracle先口,MySQL型奥,PostgreSQL

起因

在平時的的項目中瞳收,我們可能會用到對與數(shù)據(jù)進行悲觀鎖,比如select for update的語法厢汹,然后該語句默認會等待其他事務完成(提交/回滾/超時)時螟深,才會返回結(jié)果,在實際業(yè)務場景中烫葬,這不是理想的做法界弧,理論上應該在超過指定時間沒有獲取到鎖,則應該返回其他業(yè)務處理搭综,而不是一直等待垢箕。

解決

oracle

oracle的語法中,支持直接在select for update語句后面跟上[nowait | wait n]兑巾,nowait表示獲取不到鎖立即返回資源繁忙錯誤条获,wait nn表示嘗試等待n秒后蒋歌,獲取不到鎖則返回資源繁忙錯誤帅掘。

mysql

在mysql中,select id,user_name from user_info where id=1 for update no wait; 會提示語法錯誤堂油,因為mysql不支持修档,那么mysql中有個全局變量@@innodb_lock_wait_timeout,單位為秒府框,該變量表示事務等待獲取資源等待的最長時間吱窝,超過這個時間還未分配到資源則會返回應用失敗。那么這正是我們想要的迫靖,該變量可以全局指定癣诱,也可以針對每個session指定。

  • select @@innodb_lock_wait_timeout; 查詢?nèi)仲Y源等待超時時間
  • set session innodb_lock_wait_timeout=0; 設置當前會話的資源等待超時時間
# wait 1 second
SET SESSION innodb_lock_wait_timeout = 1;

SELECT id, user_name FROM user_info WHERE id = 1 FOR UPDATE;

spring data jpa

在spring data jpa中袜香,如果使用了注解@Lock(LockModeType.PESSIMISTIC_WRITE)撕予,如果需要設置超時,可以使用查詢暗語@QueryHints(value = {@QueryHint(name = "javax.persistence.lock.timeout", value = "5000")})蜈首,然后以上的設置实抡,目前只針對oracle有效,目前欢策,該參數(shù)正在提議準備修改單位為秒吆寨。

// The next query's lock attempt must fail at _some_ point, and
// we'd like to wait 5 seconds for the lock to become available:
//
// - H2 fails with a default global lock timeout of 1 second.


// - Oracle supports dynamic lock timeouts, we set it with
//   the 'javax.persistence.lock.timeout' hint on the query:
//
//      no hint == FOR UPDATE
//      javax.persistence.lock.timeout 0ms == FOR UPDATE NOWAIT
//      javax.persistence.lock.timeout >0ms == FOR UPDATE WAIT [seconds]


// - PostgreSQL doesn't timeout and just hangs indefinitely if
//   NOWAIT isn't specified for the query. One possible way to
//   wait for a lock is to set a statement timeout for the whole
//   connection/session.
//   connection.createStatement().execute("set statement_timeout = 5000");

// - MySQL also doesn't support query lock timeouts, but you
//   can set a timeout for the whole connection/session.
//  connection.createStatement().execute("set innodb_lock_wait_timeout = 5;");

spring boot custom support MySQL & PostgreSQL

package com.scio.cloud.jpa;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.QueryHint;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.MySQLDialect;
import org.hibernate.dialect.PostgreSQL81Dialect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.data.jpa.repository.QueryHints;
import org.springframework.data.jpa.repository.query.AbstractJpaQuery;
import org.springframework.data.jpa.repository.query.JpaQueryMethod;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactory;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.support.QueryCreationListener;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
import org.springframework.data.repository.core.support.RepositoryProxyPostProcessor;
import org.springframework.util.ReflectionUtils;
/**
 * Custom JpaJpaRepository Bean
 *
 * @author Wang.ch
 * @qq 18565615@qq.com
 * @date 2019-03-07 17:16:17
 * @param <T>
 * @param <S>
 * @param <ID>
 */
public class ScioJpaRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extends Serializable>
    extends JpaRepositoryFactoryBean<T, S, ID> {

  public ScioJpaRepositoryFactoryBean(Class<? extends T> repositoryInterface) {
    super(repositoryInterface);
  }
  /**
   * we can custom JpaRepositoryFactory class like addRepositoryProxyPostProcessor or
   * addQueryCreationListener etc.
   */
  @Override
  protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) {
    // return super.createRepositoryFactory(entityManager);
    JpaRepositoryFactory factory = new ScioJpaRepositoryFactory(entityManager);
    // factory.addQueryCreationListener(new ScioQueryCreationListener());
    factory.addRepositoryProxyPostProcessor(new ScioRepositoryProxyPostProcessor());
    return factory;
  }
  /**
   * Custom JpaRepositoryFactory
   *
   * @author Wang.ch
   * @date 2019-03-07 17:17:58
   */
  public class ScioJpaRepositoryFactory extends JpaRepositoryFactory {

    public ScioJpaRepositoryFactory(EntityManager entityManager) {
      super(entityManager);
    }
  }
  /**
   * Custom RepositoryProxyPostProcessor add advice to RepositoryProxy
   *
   * @author Wang.ch
   * @date 2019-03-07 17:18:13
   */
  public class ScioRepositoryProxyPostProcessor implements RepositoryProxyPostProcessor {

    @Override
    public void postProcess(ProxyFactory factory, RepositoryInformation repositoryInformation) {
      factory.addAdvice(LockTimeoutAdvice.INSTANCE);
    }
  }
  /**
   * LockTimeoutAdvice for MySQL and PostgreSQL
   *
   * @author Wang.ch
   * @date 2019-03-07 17:18:54
   */
  public enum LockTimeoutAdvice implements MethodInterceptor {
    INSTANCE;

    private static final Logger LOG = LoggerFactory.getLogger(LockTimeoutAdvice.class);
    /** hand invocation */
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
      List<QueryHint> list = getHints(invocation.getMethod());
      String str =
          list.stream()
              .filter(qh -> qh.name().equals("javax.persistence.lock.timeout"))
              .map(qh -> qh.value())
              .findFirst()
              .orElse(null);
      int lockTimeout = -1;
      if (StringUtils.isNotBlank(str)) {
        lockTimeout = NumberUtils.createInteger(str);
      }
      if (lockTimeout != -1) {
        Object target = invocation.getThis();
        // get EntityManager
        EntityManager em = getBeanProperty("em", target);
        Session session = em.unwrap(Session.class);
        SessionFactory factory = session.getSessionFactory();
        // get Dialect
        Dialect dialect = getBeanProperty("dialect", factory);
        String sql = null;
        if (MySQLDialect.class.isAssignableFrom(dialect.getClass())) {
          sql = "set innodb_lock_wait_timeout = " + (lockTimeout / 1000) + ";";
        } else if (PostgreSQL81Dialect.class.isAssignableFrom(dialect.getClass())) {
          sql = "set statement_timeout = " + lockTimeout;
        }
        if (StringUtils.isNotBlank(sql)) {
          final String lockTimeoutSql = sql;
          if (LOG.isDebugEnabled()) {
            LOG.debug("prepare to set locktimeout : {}", lockTimeoutSql);
          }
          session.doWork(s -> s.createStatement().execute(lockTimeoutSql));
        }
      }
      Object obj = invocation.proceed();
      return obj;
    }

    @SuppressWarnings("unchecked")
    private <T> T getBeanProperty(String name, Object target) throws NoSuchFieldException {
      Field field = target.getClass().getDeclaredField(name);
      ReflectionUtils.makeAccessible(field);
      Object em = ReflectionUtils.getField(field, target);
      return (T) em;
    }
    /**
     * find method hints
     *
     * @param m
     * @return
     */
    protected List<QueryHint> getHints(Method m) {
      List<QueryHint> result = new ArrayList<QueryHint>();
      QueryHints hints = AnnotatedElementUtils.findMergedAnnotation(m, QueryHints.class);
      if (hints != null) {
        result.addAll(Arrays.asList(hints.value()));
      }
      return result;
    }
  }
  /**
   * Query creation Listener
   *
   * @author Wang.ch
   * @date 2019-03-07 17:23:40
   */
  public class ScioQueryCreationListener implements QueryCreationListener<AbstractJpaQuery> {
    @Override
    public void onCreation(AbstractJpaQuery query) {
      List<QueryHint> list = getHints(query.getQueryMethod());
      if (CollectionUtils.isNotEmpty(list)) {
        list.stream().forEach(System.out::println);
      }
    }

    protected List<QueryHint> getHints(JpaQueryMethod query) {
      List<QueryHint> result = new ArrayList<QueryHint>();
      Field field = null;
      try {
        field = JpaQueryMethod.class.getDeclaredField("method");
      } catch (NoSuchFieldException | SecurityException e) {
        e.printStackTrace();
      }
      if (field == null) {
        return Collections.emptyList();
      }
      ReflectionUtils.makeAccessible(field);
      Method m = (Method) ReflectionUtils.getField(field, query);
      QueryHints hints = AnnotatedElementUtils.findMergedAnnotation(m, QueryHints.class);
      if (hints != null) {
        result.addAll(Arrays.asList(hints.value()));
      }
      return result;
    }
  }
}

  • @EnableJpaRepositories on bootstrap class
@EnableJpaRepositories(repositoryFactoryBeanClass=ScioJpaRepositoryFactoryBean.class)
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市踩寇,隨后出現(xiàn)的幾起案子啄清,更是在濱河造成了極大的恐慌,老刑警劉巖俺孙,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件辣卒,死亡現(xiàn)場離奇詭異掷贾,居然都是意外死亡,警方通過查閱死者的電腦和手機荣茫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進店門想帅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人啡莉,你說我怎么就攤上這事港准。” “怎么了咧欣?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵浅缸,是天一觀的道長。 經(jīng)常有香客問我魄咕,道長疗杉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任蚕礼,我火速辦了婚禮烟具,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘奠蹬。我一直安慰自己朝聋,他們只是感情好,可當我...
    茶點故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布囤躁。 她就那樣靜靜地躺著冀痕,像睡著了一般。 火紅的嫁衣襯著肌膚如雪狸演。 梳的紋絲不亂的頭發(fā)上言蛇,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天,我揣著相機與錄音宵距,去河邊找鬼腊尚。 笑死,一個胖子當著我的面吹牛满哪,可吹牛的內(nèi)容都是我干的婿斥。 我是一名探鬼主播,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼哨鸭,長吁一口氣:“原來是場噩夢啊……” “哼民宿!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起像鸡,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤活鹰,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體志群,經(jīng)...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡着绷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了赖舟。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蓬戚。...
    茶點故事閱讀 39,926評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡夸楣,死狀恐怖宾抓,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情豫喧,我是刑警寧澤石洗,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站紧显,受9級特大地震影響讲衫,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜孵班,卻給世界環(huán)境...
    茶點故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一涉兽、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧篙程,春花似錦枷畏、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至氮发,卻和暖如春渴肉,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背爽冕。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工仇祭, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人颈畸。 一個月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓前塔,卻偏偏與公主長得像,于是被迫代替她去往敵國和親承冰。 傳聞我的和親對象是個殘疾皇子华弓,可洞房花燭夜當晚...
    茶點故事閱讀 44,871評論 2 354

推薦閱讀更多精彩內(nèi)容