持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情
场景分析
- 在集群环境下,一个服务部署在了三台机器上,他们所提供的功能一模一样
比如在做抢单的时候,一个订单,多个司机抢,就涉及到了并发竞争的问题,这时候就需要用到锁
如果是单个服务,jvm锁是可以控制并发请求的竞争问题的
- 但是集群情况下,如果不同司机的请求落在了不同的服务上,而不同服务的锁只能锁住自己的资源。
- 比如两个服务,每个服务五个请求,这样每台服务都会有一个人抢单成功,四个人抢单失败
但是司机的请求可能落在不同的机器上,这时候jvm提供的单独程序的锁就没办法解决集群服务的问题,就需要引入分布式锁
除非把锁住的这个对象交给第三方,来统一的控制
mysql实现分布式锁
- 写一个工具类,用mysql实现lock和unlock
- 思路:通过主键冲突来做
- 将订单id设置为主键,这样只有一个司机能插入成功,其他司机通过这个订单id再次插入时候就会发生主键冲突导致插入失败(抢锁失败)
- 释放锁时,再根据主键删除掉这条记录即可
- 如果释放锁失败(也就是删除这条记录失败),可以通过mysql触发器删掉这个锁,类似于redis的设置过期时间
- 可以通过threadLocal存储锁对象
- 释放锁时,再根据主键删除掉这条记录即可
实现方式
package com.online.taxi.order.lock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import com.online.taxi.order.dao.TblOrderLockDao;
import com.online.taxi.order.entity.TblOrderLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import lombok.Data;
@Service
@Data
public class MysqlLock implements Lock {
@Autowired
private TblOrderLockDao mapper;
private ThreadLocal<TblOrderLock> orderLockThreadLocal ;
@Override
public void lock() {
// 1、尝试加锁
if(tryLock()) {
System.out.println("尝试加锁");
return;
}
// 2.休眠
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 3.递归再次调用
lock();
}
/**
* 非阻塞式加锁,成功,就成功,失败就失败。直接返回
*/
@Override
public boolean tryLock() {
try {
TblOrderLock tblOrderLock = orderLockThreadLocal.get();
mapper.insertSelective(tblOrderLock);
System.out.println("加锁对象:"+orderLockThreadLocal.get());
return true;
}catch (Exception e) {
return false;
}
}
@Override
public void unlock() {
mapper.deleteByPrimaryKey(orderLockThreadLocal.get().getOrderId());
System.out.println("解锁对象:"+orderLockThreadLocal.get());
orderLockThreadLocal.remove();
}
@Override
public void lockInterruptibly() throws InterruptedException {
// TODO Auto-generated method stub
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
// TODO Auto-generated method stub
return false;
}
@Override
public Condition newCondition() {
// TODO Auto-generated method stub
return null;
}
}
package com.online.taxi.order.service.impl;
import com.online.taxi.order.entity.TblOrderLock;
import com.online.taxi.order.lock.MysqlLock;
import com.online.taxi.order.service.GrabService;
import com.online.taxi.order.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service("grabMysqlLockService")
public class GrabMysqlLockServiceImpl implements GrabService {
@Autowired
private MysqlLock lock;
@Autowired
OrderService orderService;
ThreadLocal<TblOrderLock> orderLock = new ThreadLocal<>();
@Override
public String grabOrder(int orderId, int driverId) {
// 生成 锁
//生成key
TblOrderLock ol = new TblOrderLock();
ol.setOrderId(orderId);
ol.setDriverId(driverId);
orderLock.set(ol);
lock.setOrderLockThreadLocal(orderLock);
// lock
lock.lock();
// 执行业务
try {
System.out.println("司机:"+driverId+" 执行抢单逻辑");
boolean b = orderService.grab(orderId, driverId);
if(b) {
System.out.println("司机:"+driverId+" 抢单成功");
}else {
System.out.println("司机:"+driverId+" 抢单失败");
}
}finally {
// 释放锁
lock.unlock();
}
// 执行业务
return null;
}
}
缺点
- IO是瓶颈,如果并发量大的情况下需要频繁的对数据库进行操作,会对数据库造成很大压力,而且速度慢
redis实现分布式锁
手写redis
- 使用redis的setnx操作来实现分布式锁
- 如果存在这天记录则插入失败,如果不存在这条记录则插入成功
- 注意点:
- 得加超时时间,不然如果抢到锁的线程挂了,锁无法释放会产生死锁
- 上锁和超时时间需要一起设置,不然不是原子的,可能会出现在设置过期时间之前出问题了,这样超时时间设置失败,又会出现第一种情况
- 不能直接释放锁,不然会出现把别人的锁释放了的情况
比如锁的过期时间为10分钟,抢到锁的线程A执行时间为8分钟,在锁有效期内执行完,锁正常释放
突然有一次,抢到锁的线程A执行时间为12分钟,超过了锁的有效期,锁已经释放了,然后释放后被线程B抢到了,这时候线程A执行完释放的就是线程B的锁
解决办法:
- 自己的锁自己释放,可以在上锁的时候把value设置成当前线程的标识id(司机id),释放锁时判断下这个value是否是自己的标再进行释放即可
- 在任务没执行完的时候不要去释放锁,发起一个异步线程,对锁过期时间进行续约
实现方式
package com.online.taxi.order.lock;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import javax.annotation.Resource;
import com.online.taxi.order.entity.TblOrderLock;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Service;
import lombok.Data;
/**
* 例子中暂时不用
* @author yueyi2019
*
*/
@Service
@Data
public class RedisLock implements Lock {
@Resource
private RedisTemplate<Integer, Integer> redisTemplate;
private TblOrderLock orderLock;
@Override
public void lock() {
// 1、尝试加锁
if(tryLock()) {
return;
}
// 2.休眠
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 3.递归再次调用
lock();
}
/**
* 非阻塞式加锁,成功,就成功,失败就失败。直接返回
*/
@Override
public boolean tryLock() {
int orderId = orderLock.getOrderId();
int driverId = orderLock.getDriverId();
Boolean b = redisTemplate.opsForValue().setIfAbsent(orderId, driverId, 50, TimeUnit.SECONDS);
if(b) {
return true;
}
return false;
}
@Override
public void unlock() {
DefaultRedisScript<List> getRedisScript = new DefaultRedisScript<List>();
getRedisScript.setResultType(List.class);
getRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("luascript/lock.lua")));
redisTemplate.execute(getRedisScript, Arrays.asList(orderLock.getOrderId()), Arrays.asList(orderLock.getDriverId()));
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public Condition newCondition() {
// TODO Auto-generated method stub
return null;
}
}
package com.online.taxi.order.service.impl;
import com.online.taxi.order.service.GrabService;
import com.online.taxi.order.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
* @author yueyi2019
*/
@Service("grabRedisLockService")
public class GrabRedisLockServiceImpl implements GrabService {
@Autowired
StringRedisTemplate stringRedisTemplate;
@Autowired
OrderService orderService;
@Override
public String grabOrder(int orderId , int driverId){
//生成key
String lock = "order_"+(orderId+"");
/*
* 情况一,如果锁没执行到释放,比如业务逻辑执行一半,运维重启服务,或 服务器挂了,没走 finally,怎么办?
* 加超时时间
* setnx
*/
// boolean lockStatus = stringRedisTemplate.opsForValue().setIfAbsent(lock.intern(), driverId+"");
// if(!lockStatus) {
// return null;
// }
/*
* 情况二:加超时时间,会有加不上的情况,运维重启
*/
// boolean lockStatus = stringRedisTemplate.opsForValue().setIfAbsent(lock.intern(), driverId+"");
// stringRedisTemplate.expire(lock.intern(), 30L, TimeUnit.SECONDS);
// if(!lockStatus) {
// return null;
// }
/*
* 情况三:超时时间应该一次加,不应该分2行代码,
*
*/
boolean lockStatus = stringRedisTemplate.opsForValue().setIfAbsent(lock.intern(), driverId+"", 30L, TimeUnit.SECONDS);
// 开个子线程,原来时间N,每个n/3,去续上n
if(!lockStatus) {
return null;
}
try {
System.out.println("司机:"+driverId+" 执行抢单逻辑");
boolean b = orderService.grab(orderId, driverId);
if(b) {
System.out.println("司机:"+driverId+" 抢单成功");
}else {
System.out.println("司机:"+driverId+" 抢单失败");
}
} finally {
/**
* 这种释放锁有,可能释放了别人的锁。
*/
// stringRedisTemplate.delete(lock.intern());
/**
* 下面代码避免释放别人的锁
*/
if((driverId+"").equals(stringRedisTemplate.opsForValue().get(lock.intern()))) {
stringRedisTemplate.delete(lock.intern());
}
}
return null;
}
}
对锁进行续约
package com.online.taxi.order.service.impl;
import com.online.taxi.order.service.RenewGrabLockService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
* @author yueyi2019
*/
@Service
public class RenewGrabLockServiceImpl implements RenewGrabLockService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
@Async
public void renewLock(String key, String value, int time) {
String v = redisTemplate.opsForValue().get(key);
if (v.equals(value)){
int sleepTime = time / 3;
try {
Thread.sleep(sleepTime * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
redisTemplate.expire(key,time,TimeUnit.SECONDS);
renewLock(key,value,time);
}
}
}
基于redission实现分布式锁(单个redis场景,主从场景)
- pom文件引入redission依赖
- 配置一个bean引入redission
实现方式
package com.online.taxi.order.service.impl;
import java.util.concurrent.TimeUnit;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import com.online.taxi.order.lock.MysqlLock;
import com.online.taxi.order.lock.RedisLock;
import com.online.taxi.order.service.GrabService;
import com.online.taxi.order.service.OrderService;
/**
* @author yueyi2019
*/
@Service("grabRedisRedissonService")
public class GrabRedisRedissonServiceImpl implements GrabService {
@Autowired
RedissonClient redissonClient;
// @Autowired
// Redisson redisson;
@Autowired
OrderService orderService;
@Override
public String grabOrder(int orderId , int driverId){
//生成key
String lock = "order_"+(orderId+"");
RLock rlock = redissonClient.getLock(lock.intern());
// RLock lock1 = redisson.getLock(lock.intern());
try {
// 此代码默认 设置key 超时时间30秒,过10秒,再延时
rlock.lock();
// lock1.lock();
try {
TimeUnit.MINUTES.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
// lock1.lock();
System.out.println("司机:"+driverId+" 执行抢单逻辑");
boolean b = orderService.grab(orderId, driverId);
if(b) {
System.out.println("司机:"+driverId+" 抢单成功");
}else {
System.out.println("司机:"+driverId+" 抢单失败");
}
} finally {
rlock.unlock();
// lock1.unlock();
}
return null;
}
}
redis集群(红锁)
- 前提:独立的几台redis
- 使用红锁解决分布式锁问题,引入三台redisson,获取他们的锁
实现流程
- 实现红锁的前提条件,几台redis都是独立的不相干的
- 先获取当前时间
- 按照顺序加锁
- 加锁失败的两种情况
-
- 加锁的总时间超过了锁的有效期
- 加锁的机器未过半
- 这时候就算加锁失败,就会释放所有机器上的锁
问题点
- 假如一共五台机器,两个线程去抢锁
- 线程一获取到了三台机器的锁,过半了抢锁成功,这时候线程一持有锁
- 但是这时候redis3宕机了,而且正好没做持久化
- 与此同时线程2进来获取锁,发现1和2都有锁了,刚好3这时候重启,它就拿到345的锁,也过半,这时候就有两个线程都获取锁了
解决办法
- 让运维延时启动,这样当超过了锁的有效期或者线程1执行完后就会自动释放锁
实现方式
package com.online.taxi.order.service.impl;
import com.online.taxi.order.constant.RedisKeyConstant;
import com.online.taxi.order.service.GrabService;
import com.online.taxi.order.service.OrderService;
import org.redisson.RedissonRedLock;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
* @author yueyi2019
*/
@Service("grabRedisRedissonRedLockLockService")
public class GrabRedisRedissonRedLockLockServiceImpl implements GrabService {
@Autowired
@Qualifier("redissonRed1")
private RedissonClient redissonRed1;
@Autowired
@Qualifier("redissonRed2")
private RedissonClient redissonRed2;
@Autowired
@Qualifier("redissonRed3")
private RedissonClient redissonRed3;
@Autowired
OrderService orderService;
@Override
public String grabOrder(int orderId , int driverId){
//生成key
String lockKey = (RedisKeyConstant.GRAB_LOCK_ORDER_KEY_PRE + orderId).intern();
//redisson锁 哨兵
// RLock rLock = redisson.getLock(lockKey);
// rLock.lock();
//redisson锁 单节点
// RLock rLock = redissonRed1.getLock(lockKey);
//红锁 redis son
RLock rLock1 = redissonRed1.getLock(lockKey);
RLock rLock2 = redissonRed2.getLock(lockKey);
RLock rLock3 = redissonRed3.getLock(lockKey);
RedissonRedLock rLock = new RedissonRedLock(rLock1,rLock2,rLock3);
try {
rLock.lock();
try {
TimeUnit.MINUTES.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 此代码默认 设置key 超时时间30秒,过10秒,再延时
System.out.println("司机:"+driverId+" 执行抢单逻辑");
boolean b = orderService.grab(orderId, driverId);
if(b) {
System.out.println("司机:"+driverId+" 抢单成功");
}else {
System.out.println("司机:"+driverId+" 抢单失败");
}
} finally {
rLock.unlock();
}
return null;
}
}
- lua脚本
- 如果不用redisson可以写一个lua脚本实现,lua脚本都是原子性的
基于spring Integration实现
配合Aop基于注解的形式实现分布式锁,只需要在方法上加上注解就能实现锁
- 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-redis</artifactId>
</dependency>
- 配置注解
/**
* @author zhengtong
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisLock {
String value() default "";
int time() default 30;
}
- 配置redisLockRegistry
/**
* <>
*
* @author zhengtong
* @create 2021/7/31
* @since 1.0.0
*/
@Configuration
public class RedisLockConfiguration {
@Bean
public RedisLockRegistry getRedisLockRegistry(RedisConnectionFactory factory) {
return new RedisLockRegistry(factory, "order_locl");
}
}
- 配置切面
/**
* <>
*
* @author zhengtong
* @create 2021/7/31
* @since 1.0.0
*/
@Component
@Aspect
@Slf4j
public class LockAop {
private WebApplicationContext webApplicationContext;
public LockAop(WebApplicationContext webApplicationContext) {
this.webApplicationContext = webApplicationContext;
}
@Pointcut("@annotation(com.ztiyou.springboottest.RedisLock)")
private void apiAop() {
}
@Around("apiAop()")
public Object aroundApi(ProceedingJoinPoint proceedingJoinPoint) throws InterruptedException {
MethodSignature signature =
(MethodSignature) proceedingJoinPoint.getSignature();
Method method = signature.getMethod();
Object[] args = proceedingJoinPoint.getArgs();
RedisLock annotation = method.getAnnotation(RedisLock.class);
RedisLockRegistry redisLockRegistry =
(RedisLockRegistry) webApplicationContext.getBean(annotation.value());
Lock lock = redisLockRegistry.obtain(signature.getName() + "_" + args[0]);
Boolean b = false;
for (int i = 0; i < 3; i++) {
b = lock.tryLock(annotation.time(), TimeUnit.SECONDS);
if (b) {
break;
} else {
continue;
}
}
log.info("获取锁:" + b);
Object proceed = null;
try {
proceed = proceedingJoinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
} finally {
try {
lock.unlock();
} catch (Exception e) {
e.printStackTrace();
}
}
return proceed;
}
}
- 使用
@RedisLock(name='redisLockRegistry',time=10)
public void test(){
// 业务代码
}
上锁原理
- 每个redisLockRegistry对象内部会维护一个线程安全的Map,它的作用是用于保存名为lockKey对应的RedisLock对象。
- RedisLock里面又有一个重要的参数localLock,它是一个ReentrantLock,主要作用就是用来解决当前客户端的并发问题,Spring Integration实现的分布式锁分为两个步骤,首先线程是在当前客户端进行竞争锁资源,竞争成功后再代表当前客户端去Redis端与其他客户端进行锁竞争。
- 工作核心流程是:先竞争ReentrantLock,成功后再调用obtainLock()进行Redis端的锁竞争。 两步依次都成功后,才会返回true,表明你本次竞争锁成功。
举例说明
在这里,简单的说:
- 假设有三所学校A,B,C。每所学校有3个教师A1,A2,A3,B1,B2,B3,C1,C2,C3。 一共9个老师去教育局请教育局长来学校调研。在这里,教育局局长就是共享资源,它每次肯定只能去一所学校参观
- 首先,每所学校的3名教师会先进行内部竞争,决出一名教师代表自己的学校去教育局。最终每所学校的那名教师代表自己的学校,到达教育局,教育局局长的接待工作由秘书负责,秘书按照先后顺序接见A,B,C三所学校的代表教师。如果教育局局长有空,则由最先到的教师带走教育局局长去它的学校调研。调研结束后,教育局局长返回教育局,再由第二所学校的教师带走教育局局长。
我想这样解释就很生动的模拟了上面分布式锁的竞争过程。
⭐思考:为什么不能是9个教师直接到教育局进行先后竞争呢?
回答:
开销:每个学校派出3名老师,他们的路费就是3倍。
资源:教育局的接待数量有限,而且肯定不止一种业务(邀请教育局局长到学校调研)。9个教师同时到达教育局,教育局的接客空间是有限的,而且人多起来,可能秘书会手忙脚乱。
释放锁原理:
- 先判断当前ReentrantLock的锁是不是当前线程的
- 然后再判断Redis的锁是不是你持有的
- 没问题的话就删除key释放分布式锁
- 然后释放本地锁,允许本地其他线程去抢锁
优点
- 别人需要使用只需要引入我的jar包(AOP切面类和注解类),加上注解即可
- 用注解结合redisLockRegistry实现分布式锁,节省了团队开发成本
- 对业务代码无侵入性