文章目录

  • 1:分布式锁的概念
  • 1:概念
  • 2:锁/分布式锁/事务区别
  • 2:本文使用的案例场景
  • 1:需求
  • 2:controller层代码
  • 3:锁控制层代码(使用synchronized 不成功)
  • 4:调用的订单业务代码
  • 3:Redis解决方案-手写redis
  • 1:原理和问题优化处理
  • 2:锁机制代码-设置过期时间和只能释放自己的锁
  • 3:锁机制代码-手动增加续约时间
  • 4:Redis解决方案-redisson
  • 1:Redisson 一个用来进行分布式锁的工具类
  • 2:导入redisson依赖
  • 3:设置RedissonClient
  • 4:业务使用
  • 5:缺点



分布式锁-Redis解决方案和Redisson解决方案


分布式锁-数据库mysql解决方案


分布式锁-Redis红锁解决方案

1:分布式锁的概念

1:概念

分布式锁(多服务共享锁) 在分布式的部署环境下,通过锁机制来让多客户端互斥的对共享资源进行访问

控制分布式系统不同进程共同访问共享资源的一种锁的实现。如果不同的系统或同一个系统的不同主机之间共享了某个临界资源,往往需要互斥来防止彼此干扰,以保证一致性。

2:锁/分布式锁/事务区别

  • 锁 单进程的系统中,存在多线程同时操作一个公共变量,此时需要加锁对变量进行同步操作,保证多线程的操作线性执行消除并发修改。解决的是单进程中的多线程并发问题。
  • 分布式锁 只要的应用场景是在集群模式的多个相同服务,可能会部署在不同机器上,解决进程间安全问题,防止多进程同时操作一个变量或者数据库。解决的是多进程的并发问题 事务 解决一个会话过程中,上下文的修改对所有数据库表的操作要么全部成功,要不全部失败。所以应用在service层。解决的是一个会话中的操作的数据一致性。
  • 分布式事务 解决一个联动操作,比如一个商品的买卖分为添加商品到购物车、修改商品库存,此时购物车服务和商品库存服务可能部署在两台电脑,这时候需要保证对两个服务的操作都全部成功或者全部回退。解决的是组合服务的数据操作的一致性问题

2:本文使用的案例场景

1:需求

当在打车软件中,乘客下了订单。多个司机抢单,此时因为单子只有一个,多个司机对此共享资源进行抢,此处应该使用分布式锁;

2:controller层代码

@GetMapping("/do/{orderId}")
    public String grab(@PathVariable("orderId") int orderId, int driverId){
        System.out.println("order:"+orderId+",driverId:"+driverId);
        //此处调用锁控制层代码
        grabService.grabOrder(orderId,driverId);
        return "";
    }

3:锁控制层代码(使用synchronized 不成功)

使用synchronized 不能保证多台服务器只有一个抢成功;因为synchronized 只能锁本服务的资源;多台服务的资源是锁不住的;

@Autowired
	OrderService orderService;
	
	@Override
	public String grabOrder(int orderId, int driverId) {
		String lock = (orderId+"");
		
		synchronized (lock.intern()) {
			try {
				System.out.println("司机:"+driverId+" 执行抢单逻辑");
				//此处调用订单业务代码
	            boolean b = orderService.grab(orderId, driverId);
	            if(b) {
	            	System.out.println("司机:"+driverId+" 抢单成功");
	            }else {
	            	System.out.println("司机:"+driverId+" 抢单失败");
	            }
	            
	        } finally {
	        	
	            
	        }
		}
		
		
		return null;
	}

4:调用的订单业务代码

这一层就是写的伪代码,后续并不关注他

redis 事务 分布式锁 redis分布式事务解决方案_redis 事务 分布式锁

3:Redis解决方案-手写redis

1:原理和问题优化处理

1:判断一个固定的key在Redis里是否存在,如果这个key存在,说明这把锁正在被使用,反之说明这把锁没有被使用可以被过去

2:如果一个请求获取锁成功,但还没等释放释放锁的时候,这个服务挂掉的改怎么办?这会导致其他的服务永远也无法获取到锁,
此时可以在set key时,设置key的过期时间

3:加超时时间,会有加不上的情况,比如设置key和加过期时间不是原子操作,
此时我们可以将这两个操作变为一个方法,实现原子操作

4:在一个请求释放锁之前,key过期了,锁又被其他请求用了,然后这个请求有释放了锁,导致了释放其他请求的锁,这种情况怎么处理?
那我们可以在sey key 时,value存放针对于这个请求唯一的字符串,在释放锁之前判断是否是我创建的锁,也就是说只能释放自己的锁

5:一个请求还没执行到关键代码,锁就过期(超时)了,这又会导致并发抢锁现象,这种情况怎么处理?
我们可以使用另一个线程(守护线程),来观察这次请求是否结束,如果没有结束延长锁的过期时间(续约)

2:锁机制代码-设置过期时间和只能释放自己的锁

package com.online.taxi.order.service.impl;

import com.online.taxi.order.service.GrabService;
import com.online.taxi.order.service.OrderService;
import com.online.taxi.order.service.RenewGrabLockService;
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 
 */
@Service
public class GrabRedisLockServiceImpl implements GrabService {

	//redis调用连接
	@Autowired
	StringRedisTemplate stringRedisTemplate;
	
	@Autowired
	OrderService orderService;

	@Autowired
	RenewGrabLockService renewGrabLockService;
	
    @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+"", 10L, TimeUnit.SECONDS);
    	
    	if(!lockStatus) {
    		return null;
    	}
		try {
			TimeUnit.SECONDS.sleep(30);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

		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());
        	
        	/**
        	 * 下面代码避免释放别人的锁,通过key对应的value值来判断是否为自己的锁
        	 */
        	if((driverId+"").equals(stringRedisTemplate.opsForValue().get(lock.intern()))) {
				stringRedisTemplate.delete(lock.intern());
			}
        }
        return null;
    }
}

3:锁机制代码-手动增加续约时间

1:写一个异步的守护线程来主动为当前redis的key增加到期时间

@Service
public class RenewGrabLockServiceImpl implements RenewGrabLockService {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Override
    @Async
    public void renewLock(String key, String value, int time) {
        System.out.println("续命"+key+"  "+value);
        String v = redisTemplate.opsForValue().get(key);
        while (StringUtils.isNotBlank(v) && v.equals(value)){
            int sleepTime = time / 3;
            try {
                Thread.sleep(sleepTime * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            redisTemplate.expire(key,time,TimeUnit.SECONDS);
            
        }

    }
}

2:业务代码

@Service
public class GrabRedisLockServiceImpl implements GrabService {

	//redis调用连接
	@Autowired
	StringRedisTemplate stringRedisTemplate;
	
	@Autowired
	OrderService orderService;

	@Autowired
	RenewGrabLockService renewGrabLockService;
	
    @Override
    public String grabOrder(int orderId , int driverId){
        //生成key
    	String lock = "order_"+(orderId+"");
    
    	boolean lockStatus = stringRedisTemplate.opsForValue().setIfAbsent(lock.intern(), driverId+"", 10L, TimeUnit.SECONDS);
    	// 开个子线程,原来时间N,每个n/3,去续上n
		renewGrabLockService.renewLock(lock.intern(),driverId+"",10);
    	if(!lockStatus) {
    		return null;
    	}
		try {
			TimeUnit.SECONDS.sleep(30);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

		try {
			System.out.println("司机:"+driverId+" 执行抢单逻辑");
			
            boolean b = orderService.grab(orderId, driverId);
            if(b) {
            	System.out.println("司机:"+driverId+" 抢单成功");
            }else {
            	System.out.println("司机:"+driverId+" 抢单失败");
            }
            
        } finally {
        	        	
        	/**
        	 * 下面代码避免释放别人的锁,通过key对应的value值来判断是否为自己的锁
        	 */
        	if((driverId+"").equals(stringRedisTemplate.opsForValue().get(lock.intern()))) {
				stringRedisTemplate.delete(lock.intern());
			}
        }
        return null;
    }
}

4:Redis解决方案-redisson

1:Redisson 一个用来进行分布式锁的工具类

目前基于Redis实现的分布式锁常用的框架是Redisson,它的使用比较简单,在项目中引入Redisson的依赖,然后基于Redis实现分布式锁的加锁与释放锁,

Redisson 的加锁机制:线程1去获取锁,获取成功: 执行lua脚本,保存数据到redis数据库。线程2也去获取锁,获取失败: 一直通过while循环尝试获取锁,获取成功后,执行lua脚本,保存数据到redis数据库。

Redisson 的还有一个重要的续约机制,只要客户端一旦加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端还持有锁key,那么就会不断的延长锁key的生存时间。

Redisson 加锁工作流程如下:

redis 事务 分布式锁 redis分布式事务解决方案_分布式_02

2:导入redisson依赖

<dependency>
     <groupId>org.redisson</groupId>
     <artifactId>redisson</artifactId>
     <version>3.16.8</version>
 </dependency>

3:设置RedissonClient

@Bean
    public RedissonClient redissonClient() {
    	Config config = new Config();
    	config.useSingleServer().setAddress("127.0.0.1:6379").setDatabase(0);

    	return Redisson.create(config);
    }

4:业务使用

@Service
public class GrabRedisRedissonServiceImpl implements GrabService {

	@Autowired
	RedissonClient redissonClient;
	
	@Autowired
	OrderService orderService;
	
    @Override
    public String grabOrder(int orderId , int driverId){
        //生成key
    	String lock = "order_"+(orderId+"");
    	
    	RLock rlock = redissonClient.getLock(lock.intern());

        try {
    		// 此代码默认 设置key 超时时间30秒,过10秒,再延时
    		rlock.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();
        }
        return null;
    }
}

5:缺点

  • 客户端A从master获取到锁
  • 在master将锁同步到slave之前,master宕掉了。
  • slave节点被晋级为master节点
  • 客户端B从新的master获取到锁
  • 而客户端A也以为自己成功加了锁,此时就会导致多个客户端对一个分布式锁完成了加锁。这时就会导致各种脏数据的产生