乐观锁VS分布式锁实现抢单服务
司机开始接单,乘客填写出发地——目的地,开始下单
service-order模块
@Operation(summary="司机抢单")
@GetMapping("/robNewOrder/{driverId}/{orderId}")
public Result<Boolean> robNewOrder(@PathVariable Long driverId,@PathVariable Long orderId){
return Result.ok(orderInfoServcie.robNewOrder(driverId,orderId));
}
@Override
public Boolean robNewOrder(Long driverId,Long orderId){
/**
为了防止数据库压力过大,在saveOrderInfo添加订单的时候
需要向redis添加,redisTemplate.opsForValue().set(RedisConstant.ORDER_ACCEPT_MARK,
"0",RedisConstant.ORDER_ACCEPT_MARK_EXPIRES_TIME,
TimeUnit.MINUTES);
*/
if(redisTemplate.hasKey(RedisConstant.ORDER_ACCEPT_MARK)){
throw new GuiguException(ResultCodeEnum.COB_NEW_ORDER_FAIL);
}
//如果订单存在Redis,则修改订单状态"已经接单"
//修改条件:根据订单id+司机id
LambdaQueryWrapper<OrderInfo> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(orderInfo::getId,orderId);
OrderInfo orderInfo = orderInfoMapper.selectOne(wrapper);
orderInfo.setStatus(OrderStatus.ACCEPTED.getStats());
orderInfo.setDriverId(driverId);
orderInfo.setAccpetTime(new Date());
int rows = orderInfoMapper.updateById(orderInfo);
if(rows != 1){
throw new GuiguException(ResultCodeEnum.COB_NEW_ORDER_FAIL);
}
redisTemplate.delete(RedisConstant.ORDER_ACCEPT_MARK);
return true;
}
server-order-client远程定义模块
@GetMapping("/order/info/robNewOrder/{driverId}/{orderId}")
Result<Boolean> robNewOrder(@PathVariable("driverId") Long driverId,@PathVariable("orderId") Long orderId);
web调用模块
@Operation(summary = "司机抢单")
@GuiguLogin()
@GetMapping("/robNewOrder/{orderId}")
public Result<Boolean> robNewOrder(@PathVariable Long orderId){
Long driverId = AuthContextHolder.getUserId();
return Result.ok(orderService.robNewOrder(driverId,orderId));
}
@Override
public Boolean robNewOrder(Long driverId,Long orderId){
return orderInfoFeignClient.robNewOrder(driverId,orderId).getData();
}
本地锁VS分布式锁
乐观锁进行对抢单功能的优化
司机抢单 update order_info set status=2,driver_id=?,accept_time=> where id=? and status= 1
其中将status订单状态(等待接单)作为版本号来判断
@Override
public Boolean robNewOrder(Long driverId,Long orderId){
/**
为了防止数据库压力过大,在saveOrderInfo添加订单的时候
需要向redis添加,redisTemplate.opsForValue().set(RedisConstant.ORDER_ACCEPT_MARK,
"0",RedisConstant.ORDER_ACCEPT_MARK_EXPIRES_TIME,
TimeUnit.MINUTES);
*/
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
//其中将status订单状态(等待接单)作为版本号来判断
LambdaQueryWrapper<OrderInfo> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(OrderInfo::getId,orderId);
wrapper.eq(OrderInfo::getStatus,OrderStatus.WAITING_ACCEPT.getStatus());//以1作为条件
OrderInfo orderInfo = new OrderInfo();
orderInfo.setStatus(OrderStatus.ACCEPTED.getStatus());//改成2
orderInfo.setDriverId(driverId);
orderInfo.setAcceptTime(new Date());
int rows = orderInfoMapper.updateById(orderInfo,wrapper);
if(rows != 1){
throw new GuiguException(ResultCodeEnum.COB_NEW_ORDER_FAIL);
}
redisTemplate.delete(RedisConstant.ORDER_ACCEPT_MARK);
return true;
}
分布式锁Redisson进行对抢单功能的优化
本地锁的局限
@GetMapping("testLock")
public Result testLock(){
testService.testLock();
return Return.ok();
}
使用synchronized解决并发问题,通过jmeter工具实现模拟测试
@Override
public synchronized void testLock(){
String value = redisTemplate.opsForValue().get("num");
if(StringUtils.isBlank(value)){
return;
}
int num = Integer.parseInt(value);
retisTemplate.opsForValue().set("num",String.valueOf(++num));
}
通过idea进行对服务的复制,模拟集群场景
通过网关服务进行转发,jmeter进行车市
Redis实现分布式锁
- 通过redis的setnx命令,具有原子性
@Override
public void testLock(){
//获取锁
Boolean ifAbsent = redisTemplate.opsForValue().setIfAbsent("lock","lock");
//如果为true,表示获取锁成功
if(ifAbsent){
String value = redisTemplate.opsForValue().get("num");
if(StringUtils.isBlank(value)){
return;
}
//释放锁
redisTemplate.delete("lock");
}else {
try{
Thread.sleep(100);
this.testLock();
}catch(InterrutedException e){
e.printStackTrace();
}
}
}
如果释放锁之前,出现了异常,导致无法释放锁
解决方案:以下两种
- 将redisTemplate.delete(“lock”)释放锁放到finally中
- 为锁设置过期时间 redisTemplate.opsForValue().setIfAbsent(“lock”,“lock”,3,Time.SECONDS);
如果释放的是别人的锁
解决方案:
- UUID防止锁误删
@Override
public void testLock(){
String uuid = UUID.randomUUID().toString();
//key value 过期时间 过期时间的单位
Boolean ifAbsent = redisTemplate.opsForValue().setIfAbsent("lock",uuid,3,TimeUnit.SECONDS);
if(ifAbsent){
String value = redisTemplate.opsForValue().get("num");
if(StringUtils.isBlank(value)){
return;
}
int num = Integer.parseInt(value);
redisTemplate.opsForValue().set("num",String.valueOf(++num));
//释放锁
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();
}
}
}
判断释放锁后,锁过期自动释放
当2获取锁,1直接释放2的锁了,不能保证原子性
解决方案:
- lua脚本
@Override
public void testLock(){
String uuid = UUID.randomUUID().toString();
//key value 过期时间 过期时间的单位
Boolean ifAbsent = redisTemplate.opsForValue().setIfAbsent("lock",uuid,3,TimeUnit.SECONDS);
if(ifAbsent){
String value = redisTemplate.opsForValue().get("num");
if(StringUtils.isBlank(value)){
return;
}
int num = Integer.parseInt(value);
redisTemplate.opsForValue().set("num",String.valueOf(++num));
//释放锁(通过lua脚本)
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
redisScript.setScriptText(script);
redisScript.setResultType(Long.class);
stringRedisTemplate.execute(redisScript,Arrays.asList("lock"),uuid);
}else{
try{
Thread.sleep(100);
this.testLock();
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
Redisson实现分布式锁
- 获取锁 redissonClient.getLock(“lock”)
- 阻塞一直等待直到获取锁,获取锁之后默认过期时间30s lock.lock()
- 获取到锁,锁过期时间10s lock.lock(10,TimeUnit.SECONDS)
- 第一个参数获取锁等待时间,第二个参数获取锁过期时间 lock.tryLock(30,10,TimeUnit.SECONDS)
- 释放锁 lock.unlock()
①、引入依赖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);
}
}
③、添加Redisson分布式锁到司机抢单功能
OrderInfoServiceImpl添加分布式锁
@Autowired
private RedissonClient redissonClient;
@Override
public Boolean robNewOrder(Long driverId,Long orderId){
if(!redisTemplate.hasKey(RedisConstant.ORDER_ACCEPT_MARK)){
throw new GuiguException(ResultCodeEnum.COB_NEW_ORDER_FAIL);
}
//创建锁
RLock lock = redissonClient.getLock(RedisConstant.ROB_NEW_ORDER_LOCK + orderId);
try{
//获取锁 等待时间 过期时间 时间单位
boolean flag = lock.tryLock(10,5,TimeUnit.SECONDS);
if(flag){//获取锁成功
LambdaQueryWrapper<OrderInfo> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(OrderInfo::getId,orderId);
OrderInfo orderInfo = orderInfoMapper.selectOne(wrapper);
orderInfo.setStatus(OrderStatus.ACCEPTED.getStatus());
orderInfo.setDriverId(driverId);
orderInfo.setAccpetTime(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;
}