雖然數據庫有鎖的實現,但是有時候對於數據的操作更需要業務層控制。
這個解決的問題有次面試被問到過,當時不知道怎么解決,亂說一通,今天也算是有個解決方案了
項目中有個需求,就是訂單需要經過一層一層的審核,審核過程中當前審核人有權限審核,上一審核人有權限撤銷上一步的審核。這樣在審核過程中就需要對訂單審核權限進行控制:
- 只有當前審核人和上一審核人可以進行操作
- 當前審核人審核后上一審核人就不能撤回
- 上一審核人撤回后當前審核人就無法審核
實現上述需求,我就需要對訂單的審核/撤銷接口進行控制,即同一訂單的審核/撤銷要互斥(審核/撤銷是同一個接口)
最簡單的解決方案是在該接口的方法上使用 synchronized,這種方案解決了上述的問題,但是這種方案的問題是不同訂單的審核操作也不能同時進行。
回到問題本身,我們要解決的是同一訂單的審核操作要互斥,互斥是基於訂單的,所以只要審核接口所操作的對象不是同一訂單就不需要互斥,怎么實現呢。
我想到的第一個方案是使用redis來為每個訂單加鎖
思路是
- 當有審核的請求線程時,先通過訂單編號(訂單的唯一索引)往redis中set一組值(使用
RedisTemplate.opsForValue().setIfAbsent(key, value)
如果已經存在key,返回false且不做任何改變,不存在就將 key 的值設為 value),在這里我把訂單編號作為key,set成功后在設置一個過期時間(為了避免死鎖) - 當1返回true時代表加鎖成功,當前請求線程繼續執行,執行結束后需要釋放鎖,即刪除redis中的key
- 當1返回false時,等待,繼續執行2
這是鎖實現
1 package pers.lan.jc.compnent; 2 3 import lombok.extern.slf4j.Slf4j; 4 import org.springframework.beans.factory.annotation.Autowired; 5 import org.springframework.data.redis.core.RedisTemplate; 6 import org.springframework.stereotype.Component; 7 8 import java.util.concurrent.TimeUnit; 9 10 /** 11 * @author lan [1728209643@qq.com] 12 * @create 2018-12-01 14:12 13 * @desc redis鎖 14 */ 15 @Slf4j 16 @Component 17 public class RedisLock { 18 19 private final static String LOCK_PREFIX = "LOCK:"; 20 21 @Autowired 22 private RedisTemplate<String, String> redisTemplate; 23 24 public boolean lock(String key) { 25 while (true) { 26 try { 27 if (setIfAbsent(key)) { 28 return true; 29 } 30 Thread.sleep(100); 31 } catch (Exception e) { 32 return false; 33 } finally { 34 unlock(key); 35 } 36 } 37 } 38 39 private synchronized boolean setIfAbsent(String key) { 40 try { 41 Boolean locked = redisTemplate.opsForValue().setIfAbsent(LOCK_PREFIX + key, key); 42 if (locked != null && locked) { 43 redisTemplate.expire(LOCK_PREFIX + key, 120, TimeUnit.SECONDS); 44 return true; 45 } 46 } finally { 47 unlock(key); 48 } 49 return false; 50 } 51 52 public void unlock(String key) { 53 redisTemplate.delete(LOCK_PREFIX + key); 54 } 55 56 }
這種方案不好的地方在於,set和expire操作不是原子的,於是setIfAbsent()方法是互斥的,並發性能並不是很好
另一種方案是使用redisson
添加依賴
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.9.1</version> </dependency>
配置
package pers.lan.jc.config; import lombok.extern.slf4j.Slf4j; import org.redisson.Redisson; import org.redisson.api.RedissonClient; import org.redisson.config.Config; import org.redisson.config.SingleServerConfig; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @author lan [1728209643@qq.com] * @create 2018-12-01 16:19 * @desc redissonConfig */ @Slf4j @Configuration public class RedissonConfig { @Bean public RedissonClient redissonClient() { Config config = new Config(); config.setLockWatchdogTimeout(10000L); SingleServerConfig singleServerConfig = config.useSingleServer(); singleServerConfig.setPassword("travis"); singleServerConfig.setAddress("redis://118.25.43.205:6379"); singleServerConfig.setDatabase(0); return Redisson.create(config); } }
使用
package pers.lan.jc.controller; import org.redisson.api.RReadWriteLock; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** * @author lan [1728209643@qq.com] * @create 2018-12-01 14:32 * @desc redis鎖控制器 */ @RequestMapping("/lock") @RestController public class RedisLockController { @Autowired private RedissonClient redisson; private final static String PREFIX = "lan:"; @GetMapping("/get2") public Object lock2(@RequestParam String key) { RReadWriteLock lock = redisson.getReadWriteLock(PREFIX + key); try { lock.writeLock().lock(); Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + " ##########"); System.out.println(Thread.currentThread().getName() + " @@@@@@@@@@"); System.out.println(Thread.currentThread().getName() + " %%%%%%%%%%"); System.out.println(); System.out.println(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.writeLock().unlock(); } return "ok"; } }
redisson具體的文檔見https://github.com/redisson/redisson/wiki
項目中使用:
為了不影響原有業務和代碼冗余等,我想通過注解+AOP使用redisson加鎖,在每個接口上通過如下注解
package pers.lan.jc.annotation; import java.lang.annotation.*; /** * @author lan [1728209643@qq.com] * @create 2018-12-01 18:12 * @desc 加鎖器注解 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface Locking { String key(); }
key的值為每個接口加鎖的關鍵索引(比如訂單編號)
但是問題又來了,有的接口訂單表號是放在實體類中的,怎么引用呢?仿照spring中的Cache類注解,,通過Spring EL表達式使用,
需要加鎖的接口使用注解如下
@Locking(key = "#book.id") @CachePut(key = "#book.id") public void update(Book book) { bookMapper.update(book); }
其中CachePut注解可以忽略
切面如下
package pers.lan.jc.compnent; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.redisson.api.RReadWriteLock; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.expression.EvaluationContext; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.stereotype.Component; import pers.lan.jc.annotation.Locking; import java.lang.reflect.Method; /** * @author lan [1728209643@qq.com] * @create 2018-12-01 18:20 * @desc 鎖切面 */ @Aspect @Component @Slf4j public class LockAspect { @Autowired private RedissonClient redisson; @Pointcut("@annotation(pers.lan.jc.annotation.Locking)") public void lockAspect() { } @Around("lockAspect()") public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); Locking locking = method.getDeclaredAnnotation(Locking.class); String prefix = "lockKey:" + joinPoint.getTarget().getClass().getSimpleName() + "." + method.getName() + "."; if (locking != null) { try { ParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer(); String[] parameterNames = discoverer.getParameterNames(method); Object[] args = joinPoint.getArgs(); if (parameterNames != null) { ExpressionParser parser = new SpelExpressionParser(); EvaluationContext ctx = new StandardEvaluationContext(); int len = Math.min(args.length, parameterNames.length); for (int i = 0; i < len; i++) { ctx.setVariable(parameterNames[i], args[i]); } Object value = parser.parseExpression(locking.key()).getValue(ctx); RReadWriteLock lock = redisson.getReadWriteLock(prefix + value); log.info("正在嘗試向[" + prefix + "." + method.getName() + "]加鎖, key = " + prefix + value); try { lock.writeLock().lock(); log.info("加鎖成功,正在處理業務, key = " + prefix + value); return joinPoint.proceed(); } finally { log.info("業務處理結束,釋放鎖, key = " + prefix + value); lock.writeLock().unlock(); } } } catch (Exception e) { e.printStackTrace(); } } return joinPoint.proceed(); } }
為了不影響原有業務和各種異常,所以多了很多try catch塊,因為業務中用到的都是互斥鎖,所以這里我使用的都是writeLock實現的
都是為了給自己看,所以沒寫太詳細,哈哈哈