redisson实现基于业务的互斥锁
2018-12-02 06:17:43来源:博客园 阅读 ()
虽然数据库有锁的实现,但是有时候对于数据的操作更需要业务层控制。
这个解决的问题有次面试被问到过,当时不知道怎么解决,乱说一通,今天也算是有个解决方案了
项目中有个需求,就是订单需要经过一层一层的审核,审核过程中当前审核人有权限审核,上一审核人有权限撤销上一步的审核。这样在审核过程中就需要对订单审核权限进行控制:
- 只有当前审核人和上一审核人可以进行操作
- 当前审核人审核后上一审核人就不能撤回
- 上一审核人撤回后当前审核人就无法审核
实现上述需求,我就需要对订单的审核/撤销接口进行控制,即同一订单的审核/撤销要互斥(审核/撤销是同一个接口)
最简单的解决方案是在该接口的方法上使用 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实现的
都是为了给自己看,所以没写太详细,哈哈哈
标签:
版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有
- DES/3DES/AES 三种对称加密算法实现 2020-06-11
- SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后 2020-06-10
- Spring Boot 实现定时任务的 4 种方式 2020-06-10
- JSP+SSH+Mysql+DBCP实现的租车系统 2020-06-09
- Java实现的三种字符串反转 2020-06-09
IDC资讯: 主机资讯 注册资讯 托管资讯 vps资讯 网站建设
网站运营: 建站经验 策划盈利 搜索优化 网站推广 免费资源
网络编程: Asp.Net编程 Asp编程 Php编程 Xml编程 Access Mssql Mysql 其它
服务器技术: Web服务器 Ftp服务器 Mail服务器 Dns服务器 安全防护
软件技巧: 其它软件 Word Excel Powerpoint Ghost Vista QQ空间 QQ FlashGet 迅雷
网页制作: FrontPages Dreamweaver Javascript css photoshop fireworks Flash