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;
}