司机下单(第三次学习)分布式锁

1.1订单微服务接口
  • 在service-order模块里面创建接口
  • controller
@Operation(summary="司机抢单")
@GetMapping("/robNewOrder/{driverId}/{orderId}")
public Result<Boolean> robNewOrder(@PathVariable Long driverId,@PathVariable Long orderId){
    return Result.ok(orderInfoService.robNewOrder(driverId,orderId));
}
  • 在OrderInfoServiceImpl之前保存订单 方法修改
//乘客下单
@Override
public Long saveOrderInfo(OrderInfoForm orderInfoForm){
    //order_info添加订单数据
    OrderInfo orderInfo = new OrderInfo();
    BeanUtils.copyProperties(orderInfoForm,orderInfo);
    
    //订单号
    String orderNo = UUID.randomUUID().toString().replaceAll("-","");
	orderInfo.setOrderNo(orderNo);
    //订单状态
    orderInfo.setStatus(OrderStatus.WAITING_ACCEPT.getStatus());
    orderInfoMapper.insert(orderInfo);
    
    //记录日志
    this.log(orderInfo.getId(),orderInfo.getStatus());
    
    //向redis添加标识
    //接单标识,标识存在了说明不在等待接单状态了
    redisTemplate.opsForValue().set(RedisConstant.ORDER_ACCEPT_MARK,
                                   "0",RedisConstant.ORDER_ACCEPT_MARK_EXPIRES_TIME,TimeUnit.MINUTES);
    
    return orderInfo.getId();
}
  • service
//司机抢单
@Override
public Boolean robNewOrder(Long driverId,Long orderId){
    //判断订单是否存在,通过 Redis,减少数据库压力
    if(!redisTemplate.hasKey(RedisConstant.ORDER_ACCEPT_MARK)){
        //抢单失败
        throw new GuiguException(ResultCodeEnum.COB_NEW_ORDER_FAIL);
    }
    
    //司机抢单
    //修改order_info表订单状态值2:已经接单 + 司机id + 司机接单时间
    //修改条件:根据订单id
    LambdaQueryWrapper<OrderInfo> wrapper = new LambdaQueryWrapper<>();
    wrapper.eq(OrderInfo::getId,orderId);
    OrderInfo orderInfo = orderInfoMapper.selectOne(wrapper);
    //设置
    orderInfo.setStatus(OrderStatus.ACCEPTED.getStatus());
    orderInfo.setDriverId(driverId);
    orderInfo.setAcceptTime(new Date());
    //调用方法修改
    int rows = orderInfoMapper.updateById(orderInfo);
    if(row != 1){
        //抢单失败
        throw new GuiguException(ResultCodeEnum.COB_NEW_ORDER_FAIL);
    }
	//删除抢单标识
    redisTemplate.delete(RedisConstant.ORDER_ACCEPT_MARK);
    return true;
}

司机抢单优化方案

  • 刚才抢单接口里面,并没有考虑并发的问题,所以,产生问题,类似于电商里面超卖问题
  • 超卖问题
  • 解决方案

第一种 设置数据库事务的隔离级别 ,设置为Serializable,效率低下

第二种 使用乐观锁解决,通过版本号进行控制

第三种 加锁解决 ,学习过synchronized及lock锁,本地锁,目前微服务架构,分布式部署方式。

我们使用分布式锁方式解决相关问题

1.基于Redis做 分布式锁

基于REDIS的SETNX()、expire()方法做分布式锁

(1)、setnx(lockkey,1)如果返回0,则说明占位失败;如果返回1,则说明占位成功

(2)、expire()命令对lockkey设置超时时间,为的是避免死锁问题

(3)、执行完成业务代码,可以通过delete命令删除key

2.基于REDISSON做分布式锁

redisson是redis官方的分布式锁组件。

3基于乐观锁解决司机抢单

3.1基本实现思路

  • 基础sql语句,实现抢单

    update order_info set status = 2,driver_id =? ,accept_time = ? where id = ?
    

    如果使用上面语句,产生问题:只要订单接单标识没有删除,如果有很多线程请求过来,都会去更新sql语句,造成,最后提交的把之前提交数据覆盖

    • 使用乐观锁解决,添加版本号

      update order_info set status = 2,driver_id = ?,accept_time = ? where id = ? and status = 1		
      

在where后面添加条件,status = 1,相当于添加版本号

3.2代码修改

//司机抢单:乐观锁方案解决并发问题
public Boolean robNewOrder1(Long driverId,Long orderId){
    //判断订单是否在,通过Redis,减少数据库压力
    if(!redisTemplate.hasKey(RedisConstant.ORDER_ACCEPT_MARK)){
        //抢单失败
        throw new GuiguException(ResultCodeEnum.COB_NEW_ORDER_FAIL);
    }
    
    //司机抢单
    //update order_info set status = 2,driver_id = ? ,accept_time = ?
    //where id = ? and status = 1
    LambdaQueryWrapper<OrderInfo> wrapper = new LambdaQueryWrapper<>();
    wrapper.eq(OrderInfo::getId,orderId);
    wrapper.eq(OrderInfo::getStatus,OrderStatus.WAITING_ACCEPT.getStatus());
    
    //修改值
    OrderInfo orderInfo = new OrderInfo();
    orderInfo.setStatus(OrderStatus.ACCEPTED.getStatus());
    orderInfo.setDriverId(driverId);
    orderInfo.setAcceptTime(new Date());
    
    //调用方法修改
    int rows = orderInfoMapper.update(orderInfo,wrapper);
    if(rows != 1){
        //抢单失败
        throw new GuiguException(ResultCodeEnum.COB_NEW_ORDER_FAIL);
    }
    
    //删除抢单标识
    redisTemplate.delete(RedisConstant.ORDER_ACCEPT_MARK);
    return true;
}

4分布式锁解决司机抢单(重点)

本地锁的局限性

  • 之前学习过锁机制,synchronized及lock锁,都是本地锁,只在当前jvm生效
  • 举例演示
在service-order模块里面创建测试Controller
@Tag(name = "测试接口")
@RestController
@RequestMapping("/order/test")
public class TestController{
		@Autowired
		private TestService testService;
    
    	@GetMapping("testLock")
    	public Result testLock(){
            testService.testLock();
            return Result.ok();
        }
}

service

@Service
public class TestServiceImpl implements TestService{
    @Autowired
	private StringRedisTemplate redisTemplate;
    
    @Override
    public synchronized void testLock(){
        //从redis里面获取数据
        String value = redisTemplate.opsForValue().get("num");
        
        if(StringUtils.isBlank(value)){
            return;
        }
        //把从redis获取数据+1
        int num = Integer.parseInt(value);
        
        //数据+1之后放回到redis里面
        redisTemplate.opsForValue().set("num",String.valueOf(++num));
    }
}

在redis添加 初始化值,num = 0

  • 测试,模拟100请求并发过程

  • 使用测试工具jmeter实现功能测试

  • 上面测试方式,在一个服务器里面进行的测试,单机版服务。比如部署集群效果,锁还会生效?

    使用idea部署集群效果
    第一步 修改nacos配置中心端口配置,端口部分注释掉
  • 第二步 项目配置文件中添加端口号 bootstrap.properties

  • 第三步操作idea添加多个端口号

  • 第四步 启动集群服务,还 需要启动网关服务

  • 网关模块配置文件添加路由规则

  • 修改测试工具端口号,是网关端口号

实现分布式锁-Redis

1setnx+过期时间实现
@Override
public void testLock(){
    //从redis里面获取数据
    //1 获取当前锁 setnx
    Boolean ifAbsent = 
        redisTemplate.opsForValue()
        .setIfAbsent("lock","lock");
    
    //2 如果获取到锁,从redis获取数据  数据  + 1 返回redis里面
    if(ifAbsent){
        //获取锁成功,执行业务代码
        //1.先从redis中通过key num 获取值 key提前手动设置 num初始值 :0
        String value = redisTemplate.opsForValue().get("num");
        ///2.如果值为空则非法值直接返回即可
        if(StringUtils.isBlank(value)){
			return;
        }
        //3.对num值进行自增加一
        int num = Integer.parseInt(value);
        redisTemplate.opsForValue().set("num",String.valueOf(++num));
        
        
        //3释放锁
        redisTemplate.delete("lock");
        
    }else{
        try{
            Thread.sleep(100);
            this.testLock();
        }catch(InterruptedException e){
            e.printStackTrace();
        }
    }
}
  • 问题

  • 如果业务执行过程中心出现异常,导致锁无法释放

  • 解决方案:

  • 设置过期时间,到时间之后自动释放锁

    //1获取当前锁 setnx + 设置过期时间
    //Boolean ifAbsent = redisTemplate.opsForValue().setIfAbsent("lock","lock");
    Boolean ifAbsent = 
    redisTemplate.opsForValue().
    .setIfAbsent("lock","lock",10,TimeUnit.SECONDS);
    

    2UUID防止误删

    • 使用setnx + 过期时间实现分布式锁,存在问题:删除不是自己的锁,锁误删

    场景:如果业务逻辑的执行时间是7s。执行流程如下

    1.index1业务逻辑没执行完,3秒后锁被自动释放。

    2.index2获取到锁,执行业务逻辑,3秒后锁被自动释放。

    3.index3获取到锁,执行业务逻辑

    .index1业务逻辑执行完成,开始调用del释放的是index3的锁,导致index3的业务只执行1s就被别人释放。

    最终等于没锁的情况

    场景:如果业务逻辑的执行时间是7s。执行流程如下:

    //uuid防止误删
    @Override
    public void testLock(){
        //从redis里面获取数据
        String uuid = UUID.randomUUID().toString();
        //1.获取当前锁 setnx + 设置过期时间
        // Boolean ifAbsent = redisTemplate.opsForValue().setIfAbsent("lock","lock");
        Boolean ifAbsent = redisTemplate.opsForValue()
        .setIfAbsent("lock",uuid,3,TimeUnit.SECONDS);
        
        //2.如果获取到锁,从redis获取数据 数据+1 放回redis里面
        if(ifAbsent){
            //获取锁成功,执行业务代码
            //1.先从redis中通过key num 获取值 key提前手动设置 num初始值:0
            String value = redisTemplate.opsForValue().get("num");
            //2.如果值为空则非法直接返回即可
            if(StringUtils.isBlank(value)){
                return ;
            }
            //3.对num值进行自增加一
            int num = Integer.parseInt(value);
            redisTemplate.opsForValue().set("num",String.valueOf(++num));
            //出现异常
            
            //3.释放锁
            String redisUUid = redisTemplate.opsForValue().get("lock");
            if(uuid.equals(redisUUid)){
                redisTemplate.delete("lock");
            }
        }else{
            try{
                Thread.sleep(100);
                this.testLock();
            }catch(InterruptedException e){
                e.printStackTrace();
            }
        }
    }
    

    3 LUA脚本保证原子性

    • 通过uuid防止误删,但是还是存在问题,不具备原子性的
    //3释放锁
    String redisUUid = redisTemplate.opsForValue().get("lock");
    if(uuid.equals(redisUUid)){
    	redisTemplate.delete("lock");
    }
    

    index1 要锁释放,首先判断,假如判断锁就是index1,if通过了

    要进入第二行时候,没还有进入,出现问题:

    if判断通过了,这个时候锁恰好过期了,自动释放了

    index2获取当前锁

		//lua脚本保证原子性
@Override
public void testLock(){
    //从redis里面获取数据
    String uuid = UUID.randomUUID().toString();
    //1.获取当前锁 setnx + 设置过期时间
    //Boolean ifAbsent  = redisTemplate.opsForValue().setIfAbsent("lock","lock");
    Boolean ifAbsent =
        redisTemplate.opsForValue()
        .setIfAbsent("lock",uuid,3,TimeUnit.SECONDS);
    
    //2如果获取到锁,从redis获取数据, 数据+1 放回redis里面
    if(ifAbsent){
        //获取锁成功,执行业务代码
        //1.先从redis中通过key num获取值  key提前手动设置 num初始值 : 0
        String value = redisTemplate.opsForValue().get("num");
        //2.如果值为空则非法直接返回即可
        if(StringUtils.isBlank(value)){
			return;
        }
        //3.对num值进行自增加一
        int num = Integer.parseInt(value);
        redisTemplate.opsForValue().set("num",String.valueOf(++num));
        
        //出现异常
        
        //3 释放锁 lua脚本实现
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        //lua脚本
        String script = "if redis.call(\"get\",KEYS[1])== ARGV[1]\n"+
            "then\n"+
            "	return redis.call(\"del\",KEYS[1])\n" +
            "else\n"+
            "	return 0\n"+
            "end";
        redisScript.setScriptText(script);
        //设置返回结果
        redisScript.setResultType(Long.class);
        redisTemplate.execute(redisScript,Arrays.asList("lock"),uuid);
    }else{
        try{
            Thread.sleep(100);
            this.testLock();
        }catch(InterruptedException e){
            e.printStackTrace();
        }
    }
}

总结:

1、加锁

//1.从Redis中获取锁,set k1 v1 px 20000 nx
String uuid = UUID.randomUUID().toString();
Boolean lock = this.redisTemplate.opsForValue()
.setIfAbsent("lock",uuid,2,TimeUnit.SECONDS);

2.使用lua释放锁

//2.释放锁 del
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0   end";
//设置lua脚本返回的数据类型
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
//设置lua脚本返回类型为Long
redisScript.setResultType(Long.class);
redisScript.setScriptText(script);
redisTemplate.execute(redisScript,Arrays.asList("lock"),uuid);

3、重试

Thread.sleep(500)
    testLock();

为了确保分布式锁可用,我们至少要确保锁的实现同时满足一下四个条件:

第一个:互斥性,在任何时刻,只有一个客户端能持有锁。

第二个:不会发生死锁,即使有一个客户端在获取锁操作时候崩溃了,也能保证其他客户端能获取到锁。

第三个:解铃还须系铃人,解锁加锁必须同一个客户端操作。

第四个:加锁和解锁必须具备原子性

实现分布式锁-Redisson

1.概述

  • Redission是一个在Redis的基础上实现的Java驻内存数据网路(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。
  • Redission的宗旨是:促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。

引入依赖

  • 在common里面service-util引入依赖

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

    创建Redisson配置类

    @Data
    @Configuration
    @ConfigurationProperties(prefix="spring.data.redis")
    public class RedissonConfig{
        private String host;
        private String password;
        private String port;
        
        private int timeout = 3000;
        private static String ADDRESS_PREFIX = "redis://";
        
        /*
        	自动装配
        */
        @Bean
        RedissonClient redissonSingle(){
            Config config = new Config();
            
            if(!StringUtils.hasText(host)){
                throw new RuntimeException("host is empty");
            }
            SingleServerConfig serverConfig = config.useSingleServer()
                .setAddress(ADDRESS_PREFIX + this.host+":"+port)
                .setTimeout(this.timeout);
            if(StringUtils.hasText(this.password)){
                serverConfig.setPassword(this.password);
            }
            return Redisson.create(config);
        }
    }
    

3代码实现

在业务方法编写加锁和解锁

@Autowired
private RedissonClient redissonClient;

//Redisson实现
@Override
public void testLock(){
    
    //1通过redisson创建锁对象
    RLock lock = redissonClient.getLock("lock1");
    
    //2尝试获取锁
    //(1)阻塞一直等待直到获取到,获取锁之后,默认过期时间30s
    lock.lock();
    
    //(2)获取到锁,锁过期时间10s
    //lock.lock(10,TimeUnit.SECONDS)
    
    //(3)第一个参数获取锁等待时间
    //第二个参数获取到锁,锁过期时间
    //try{
    //		//true
    // boolean tryLock = lock.tryLock(30,10,TimeUnit.SECONDS);
    //} catch(InterruptedException e)
    // throw new RuntimeException(e);
    //}
    //3编写业务代码
    //1.先从redis中通过key num获取值 key提前手动设置  num 初始值:0
    String value = redisTemplate.opsForValue().get("num");
    //2.如果值为空,,则非法直接返回即可
    if(StringUtils.isBlank(value)){
        return;
    }
    //3.对num值进行自增加一
    int num = Integer.parseInt(value);
    redisTemplate.opsForValue().set("num",String.valueOf(++num));
    
    //4.释放锁
    lock.unlock();
}
}

添加Redisson分布式锁到司机抢单

在OrderInfoServiceImpl添加分布式锁

//Redisson分布式锁
//司机抢单
@Override
public Boolean robNewOrder(Long driverId,Long orderId){
    //判断订单是否存在,通过Redis,减少数据库压力
    if(!redisTemplate.hasKey(RedisConstant.ORDER_ACCEPT_MARK)){
        //抢单失败
        throw new GuiguException(ResultCodeEnum.COB_NEW_ORER_FAIL);
    }
    //创建锁
    RLock lock = redissonClient.getLock(RedisConstant.ROB_NEW_ORDER_LOCK+orderId);
    
    try{
        //获取锁
        boolean flag =
            lock.tryLock(RedisConstant.ROB_NEW_ORDER_LOCK_WAIT_TIME,
                        RedisConstant.ROB_NEW_ORDER_LOCK_LEASE_TIME,
                        TimeUnit.SECONDS);
        if(flag){
            if(!redisTemplate.hasKey(RedisConstant.ORDER_ACCEPT_MARK)){
                //抢单失败
                throw new GuiguException(ResultCodeEnum.COB_NEW_ORDER_FAILT);
            }
            //司机抢单
            //修改order_info表订单状态值2:已经接单+司机id + 司机接单时间
            //修改条件:根据订单id
            LambdaQueryWrapper<OrderInfo> wrapper = new LambdaQueryWrapper<>();;
            wrapper.eq(OrderInfo::getId,orderId);
            OrderInfo orderInfo = orderInfoMapper.selectOne(wrapper);
            //设置
            orderInfo.setStatus(OrderStatus.ACCEPTED.getStatus());
            orderInfo.setAcceptTime(new Date());
            
            //调用方法修改
            int rows = orderInfoMapper.updateById(orderInfo);
            if(rows!= 1){
                //抢单失败
                throw new GuiguException(ResultCodeEnum.COB_NEW_ORDER_FAIL);
            }
            //删除抢单标识
            redisTemplate.delete(RedisConstant.ORDER_ACCEPT_MARK);
        }
    }catch(Exception e){
        //抢单失败
        throw new GuiguException(ResultCodeEnum.COB_NEW_ORDER_FAIL);
    }finally{
        //释放
        if(lock.isLocked()){
            lock.unlock();
        }
    }
    return true;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值