虽然数据库有锁的实现,但是有时候对于数据的操作更需要业务层控制。
这个解决的问题有次面试被问到过,当时不知道怎么解决,乱说一通,今天也算是有个解决方案了
项目中有个需求,就是订单需要经过一层一层的审核,审核过程中当前审核人有权限审核,上一审核人有权限撤销上一步的审核。这样在审核过程中就需要对订单审核权限进行控制:
- 只有当前审核人和上一审核人可以进行操作
- 当前审核人审核后上一审核人就不能撤回
- 上一审核人撤回后当前审核人就无法审核
实现上述需求,我就需要对订单的审核/撤销接口进行控制,即同一订单的审核/撤销要互斥(审核/撤销是同一个接口)
最简单的解决方案是在该接口的方法上使用 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实现的
都是为了给自己看,所以没写太详细,哈哈哈