学习下分布式锁

116 阅读10分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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操作来实现分布式锁
 -   如果存在这天记录则插入失败,如果不存在这条记录则插入成功
  • 注意点:
  1. 得加超时时间,不然如果抢到锁的线程挂了,锁无法释放会产生死锁
  2. 上锁和超时时间需要一起设置,不然不是原子的,可能会出现在设置过期时间之前出问题了,这样超时时间设置失败,又会出现第一种情况
  3. 不能直接释放锁,不然会出现把别人的锁释放了的情况
  比如锁的过期时间为10分钟,抢到锁的线程A执行时间为8分钟,在锁有效期内执行完,锁正常释放
  突然有一次,抢到锁的线程A执行时间为12分钟,超过了锁的有效期,锁已经释放了,然后释放后被线程B抢到了,这时候线程A执行完释放的就是线程B的锁

解决办法

  1. 自己的锁自己释放,可以在上锁的时候把value设置成当前线程的标识id(司机id),释放锁时判断下这个value是否是自己的标再进行释放即可
  2. 在任务没执行完的时候不要去释放锁,发起一个异步线程,对锁过期时间进行续约

实现方式

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,获取他们的锁

实现流程

  1. 实现红锁的前提条件,几台redis都是独立的不相干的
  2. 先获取当前时间
  3. 按照顺序加锁
  4. 加锁失败的两种情况
    1. 加锁的总时间超过了锁的有效期
    2. 加锁的机器未过半
  1. 这时候就算加锁失败,就会释放所有机器上的锁

问题点

  • 假如一共五台机器,两个线程去抢锁
  • 线程一获取到了三台机器的锁,过半了抢锁成功,这时候线程一持有锁
  • 但是这时候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=10public 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实现分布式锁,节省了团队开发成本
  • 对业务代码无侵入性

基于ETCD实现分布式锁

基于zookeeper实现分布式锁

实现原理

实现步骤