虽然数据库有锁的实现,但是有时候对于数据的操作更需要业务层控制。

这个解决的问题有次面试被问到过,当时不知道怎么解决,乱说一通,今天也算是有个解决方案了

项目中有个需求,就是订单需要经过一层一层的审核,审核过程中当前审核人有权限审核,上一审核人有权限撤销上一步的审核。这样在审核过程中就需要对订单审核权限进行控制:

  1. 只有当前审核人和上一审核人可以进行操作
  2. 当前审核人审核后上一审核人就不能撤回
  3. 上一审核人撤回后当前审核人就无法审核

实现上述需求,我就需要对订单的审核/撤销接口进行控制,即同一订单的审核/撤销要互斥(审核/撤销是同一个接口)

最简单的解决方案是在该接口的方法上使用 synchronized,这种方案解决了上述的问题,但是这种方案的问题是不同订单的审核操作也不能同时进行。

回到问题本身,我们要解决的是同一订单的审核操作要互斥,互斥是基于订单的,所以只要审核接口所操作的对象不是同一订单就不需要互斥,怎么实现呢。

我想到的第一个方案是使用redis来为每个订单加锁

思路是

  1. 当有审核的请求线程时,先通过订单编号(订单的唯一索引)往redis中set一组值(使用

    RedisTemplate.opsForValue().setIfAbsent(key, value)

    如果已经存在key,返回false且不做任何改变,不存在就将 key 的值设为 value),在这里我把订单编号作为key,set成功后在设置一个过期时间(为了避免死锁)

  2. 当1返回true时代表加锁成功,当前请求线程继续执行,执行结束后需要释放锁,即删除redis中的key
  3. 当1返回false时,等待,继续执行2

这是锁实现

 package pers.lan.jc.compnent;

 import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component; import java.util.concurrent.TimeUnit; /**
* @author lan [1728209643@qq.com]
* @create 2018-12-01 14:12
* @desc redis锁
*/
@Slf4j
@Component
public class RedisLock { private final static String LOCK_PREFIX = "LOCK:"; @Autowired
private RedisTemplate<String, String> redisTemplate; public boolean lock(String key) {
while (true) {
try {
if (setIfAbsent(key)) {
return true;
}
Thread.sleep(100);
} catch (Exception e) {
return false;
} finally {
unlock(key);
}
}
} private synchronized boolean setIfAbsent(String key) {
try {
Boolean locked = redisTemplate.opsForValue().setIfAbsent(LOCK_PREFIX + key, key);
if (locked != null && locked) {
redisTemplate.expire(LOCK_PREFIX + key, 120, TimeUnit.SECONDS);
return true;
}
} finally {
unlock(key);
}
return false;
} public void unlock(String key) {
redisTemplate.delete(LOCK_PREFIX + key);
} }

这种方案不好的地方在于,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实现的

都是为了给自己看,所以没写太详细,哈哈哈

redisson实现基于业务的互斥锁的更多相关文章

  1. 基于Zookeeper实现的分布式互斥锁 - InterProcessMutex

    Curator是ZooKeeper的一个客户端框架,其中封装了分布式互斥锁的实现,最为常用的是InterProcessMutex,本文将对其进行代码剖析 简介 InterProcessMutex基于Z ...

  2. 图解AQS的设计与实现,手摸手带你实现一把互斥锁!

    AQS是并发编程中非常重要的概念,它是juc包下的许多并发工具类,如CountdownLatch,CyclicBarrier,Semaphore 和锁, 如ReentrantLock, ReaderW ...

  3. 基于redis的分布式锁实现方案--redisson

    实例代码地址,请前往:https://gitee.com/GuoqingLee/distributed-seckill redis官方文档地址,请前往:http://www.redis.cn/topi ...

  4. 基于(Redis | Memcache)实现分布式互斥锁

    设计一个缓存系统,不得不要考虑的问题就是:缓存穿透.缓存击穿与失效时的雪崩效应. 缓存击穿 缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则 ...

  5. 基于 Redis 的分布式锁

    前言 分布式锁在分布式应用中应用广泛,想要搞懂一个新事物首先得了解它的由来,这样才能更加的理解甚至可以举一反三. 首先谈到分布式锁自然也就联想到分布式应用. 在我们将应用拆分为分布式应用之前的单机系统 ...

  6. 基于redis的分布式锁(转)

    基于redis的分布式锁 1 介绍 这篇博文讲介绍如何一步步构建一个基于Redis的分布式锁.会从最原始的版本开始,然后根据问题进行调整,最后完成一个较为合理的分布式锁. 本篇文章会将分布式锁的实现分 ...

  7. 基于redis的分布式锁(不适合用于生产环境)

    基于redis的分布式锁 1 介绍 这篇博文讲介绍如何一步步构建一个基于Redis的分布式锁.会从最原始的版本开始,然后根据问题进行调整,最后完成一个较为合理的分布式锁. 本篇文章会将分布式锁的实现分 ...

  8. redis系列:基于redis的分布式锁

    一.介绍 这篇博文讲介绍如何一步步构建一个基于Redis的分布式锁.会从最原始的版本开始,然后根据问题进行调整,最后完成一个较为合理的分布式锁. 本篇文章会将分布式锁的实现分为两部分,一个是单机环境, ...

  9. 【连载】redis库存操作,分布式锁的四种实现方式[一]--基于zookeeper实现分布式锁

    一.背景 在电商系统中,库存的概念一定是有的,例如配一些商品的库存,做商品秒杀活动等,而由于库存操作频繁且要求原子性操作,所以绝大多数电商系统都用Redis来实现库存的加减,最近公司项目做架构升级,以 ...

随机推荐

  1. 组合数+逆元 A - Chat Group Gym - 101775A

    题目链接:https://cn.vjudge.net/contest/274151#problem/A 具体思路:我们可以先把所有的情况算出来,为2^n.然后不合法的情况减去就可以了.注意除法的时候要 ...

  2. 不老的神器:安全扫描器Nmap渗透使用指南【转】

    介绍 nmap是用来探测计算机网络上的主机和服务的一种安全扫描器.为了绘制网络拓扑图Nmap的发送特制的数据包到目标主机然后对返回数据包进行分析.Nmap是一款枚举和测试网络的强大工具. 特点 主机探 ...

  3. 19.Remove Nth Node From End of List---双指针

    题目链接 题目大意:删除单链表中倒数第n个节点.例子如下: 法一:双指针,fast指针先走n步,然后slow指针与fast一起走,记录slow前一个节点,当fast走到链表结尾,slow所指向的指针就 ...

  4. JSOI 2017 退役记

    意料之中,真的要退役了. 懒得写游记了. Round 2 的时候状态一直不太清醒,最后混了个rank19,准备AFO吧.

  5. 设计模式之笔记--职责链模式(Chain of Responsibility)

    职责链模式(Chain of Responsibility) 定义 职责链模式(Chain of Responsibility),使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系 ...

  6. acm专题---拓扑排序+优先队列

    struct node{ int id; int cnt; node(int _id,int _cnt):id(_id),cnt(_cnt){} bool operator<(node a) c ...

  7. Linux下如何创建新用户

    Linux下如何创建新用户 Linux系统中,只有root用户有创建其他用户的权限.创建过程如下:   useradd -d /home/newuser newuser(设定了该用户的主目录和用户名) ...

  8. mysql 5.7 表名大小写不敏感

    修改 my.cnf 添加 [mysqld] lower_case_table_names=1 重启MySQL service mysqld restart

  9. 在Ubuntu上安装Arena

    安装JDK 首先安装JDK对吧,下面以jdk-7u67-linux-i586.tar.gz为例 在官网上下载JDK,具体依照你的机器而定. 解压掉 tar -zxvf jdk-7u67-linux-i ...

  10. [实战]MVC5+EF6+MySql企业网盘实战(14)——逻辑重构

    写在前面 上篇文章关于修改文件夹和文件名称导致的找不到物理文件的问题,这篇文章将对其进行逻辑的修改. 系列文章 [EF]vs15+ef6+mysql code first方式 [实战]MVC5+EF6 ...