mmall_v2.0 分布式锁实现定时关单

栏目: 数据库 · 发布时间: 6年前

内容简介:在电商项目中,用户购买商品在生成订单后,一定时间内如果没有付款,应该将订单关闭。这里,主要用

在电商项目中,用户购买商品在生成订单后,一定时间内如果没有付款,应该将订单关闭。这里,主要用 Spring Schedule 和分布式锁来实现,而分布式锁也分别用 Redis 命令原生实现和 Redisson 框架两种方式。

Spring Schedule 介绍

Spring Schedule 是一个任务调度框架,用于定时任务调度等。主要通过 @Scheduled 注解来创建定时任务,可通过 cron 表达式来指定任务特定的执行时间。

Cron 表达式

Cron 表达式是一个字符串,由 67 个字段组成,对应为 秒、分、时、日、月、周、年(可选)。

允许的值和特殊字符

字段名 允许的值 允许的特殊字符
0-59 , - * /
0-59 , - * /
0-23 , - * /
月内第几天 1-31 , - * / ? L W C
1-12 或 JAN-DEC , - * /
周内第几天 1-7 或 SUN-SAT , - * / ? L C #
年(可选) 留空,1970-2099 , - * /

特殊字符的含义

  • * :匹配任意值,例如秒域为 * 表示每秒都会触发事件;
  • ? : 只能在月内第几天和周内第几天两个域使用,用于执行不明确的值。当两个域之一被指定值后,为避免冲突,需要将另一个的值设为 ?
  • - : 指定一个范围,例如分域为 3-6 ,表示从 3 分到 6 分钟每分钟触发一次;
  • / : 指定增量,表示起始时间开始触发,然后每隔固定时间触发一次,例如分域为 5/15 ,则意味着 5 分、 20 分、 35 分、 50 分,分别触发一次;
  • , :指定几个可选值。例如在分域使用 5,15 ,则意味着在 520 分各触发一次;
  • L : 表示最后,只能出现在周内第几天和月内第几天域,表示一月的最后一天,或一周的最后一天。如果在周内第几天域前加上数字,表示一月的最后一个第几天。例如 5L 表示一个月的最后一个周五;
  • W : 指定有效工作日(周一到周五),只能在月内第几天域使用,系统将在离指定日期的最近的有效工作日触发。注意一点, W 的最近寻找不会跨过月份;
  • LW : 两个字符可以连用,表示一月的最后一个工作日,即最后一个星期五。
  • # : 指定一月的周内第几天,只能出现在月内第几天域。例如在 2#3 ,表示一月的第三个星期一( 2 表示周一, 3 表示第三周)。
  • C :可以在月内第几天和周内第几天使用。

举例

"0 1 * * * *"               表示每小时1分0秒执行一次

"*/20 * * * * *"            表示每20秒执行一次

"0 0 9-12 * * *"            表示每天9,10,11,12点执行一次

"0 0/20 9-12 * * *"         表示每天9点到12点,每20分钟执行一次

"0 0 9-12 * * 2-6"          表示每周一至周五,9点到12点的0分0秒执行一次

"0 0 0 1 4 ?"               表示4月1日0时0分0秒执行一次
复制代码

实现定时任务

首先,在 applicationContext.xml 文件中配置:

<task:annotation-driven/>
复制代码

开启定时任务。注意,导入约束时导入的是 http://www.springframework.org/schema/task

然后,创建定时关闭订单的 task

@Component
@Slf4j
public class CloseOrderTask {

    @Autowired
    private IOrderService iOrderService;

    @Scheduled(cron="0 */1 * * * ?")
    public void closeOrderTaskV1() {
        log.info("关闭订单定时任务启动");
        int hour = Integer.parseInt(PropertiesUtil.getProperty("close.order.task.time.hour", "1"));
        iOrderService.closeOrder(hour);
        log.info("关闭订单定时任务结束");
    }
}
复制代码

表示每隔一分钟就查看是否有超过一个小时的订单未付款,如果有则进行关闭。 IOrderServiceImplcloseOrder 方法如下:

@Override
public void closeOrder(int hour) {
    Date closeDateTime = DateUtils.addHours(new Date(), -hour);
    List<Order> orderList = orderMapper.selectOrderStatusByCreateTime(Const.OrderStatusEnum.NO_PAY.getCode(), DateTimeUtil.dateToStr(closeDateTime));
    for (Order order : orderList) {
        List<OrderItem> orderItemList = orderItemMapper.getByOrderNo(order.getOrderNo());
        for (OrderItem orderItem : orderItemList) {
            Integer stock = productMapper.selectStockByProductId(orderItem.getId());
            if (stock == null) {
                continue;
            }
            Product product = new Product();
            product.setId(orderItem.getProductId());
            product.setStock(stock + orderItem.getQuantity());
            productMapper.updateByPrimaryKeySelective(product);
        }
        orderMapper.closeOrderByOrderId(order.getId());
        log.info("关闭订单OrderNo:{}", order.getOrderNo());
    }
}
复制代码

主要逻辑是,首先查询超过一个小时的订单列表,然后对列表中的每一条订单,根据订单号查询商品列表,对每一件商品的库存进行更新,最后,对订单的状态进行修改,即意味着删除。

如此,定时关闭一定时间内未付款的订单的 v1 版本就完成了。但是在 tomcat 集群环境下,每次只需要一台机器执行即可,不用每台机器都执行;而且,多台机器同时执行也容易造成数据错乱。所以,这就需要使用分布式锁来进行保证。

Redis 命令实现分布式锁

Redis 命令

下面是其中会用到的一下 Redis 命令:

1). setnx key value

SET if Not eXists 的简称。如果键不存在,则将键 key 的值设置为 value 。否则如果键已经存在,则不做任何操作。

设置成功时返回 1 ,设置失败时返回 0

2). getset key value

将键 key 的值设为 value ,并返回键 key 在被设置之前的旧值。

如果键 key 存在旧值,则会返回。否则如果不存在旧值,也就是键 key 在设置之前并不存在,则返回 nil

3). expire key seconds

为给定的键 key 设置生存时间,当 key 的生存时间为 0 (过期) 时,它会被自动删除。

4). del key [key...]

删除给定的一个或多个 key

Redis 分布式锁

Redis 分布式锁原理

Redis 分布式锁的流程图如下:

mmall_v2.0 分布式锁实现定时关单

它的主要原理是:首先,通过 setnx 存入一个 lockkey ,如果设置成功,也就是获取锁成功,就为锁设置一个有效期,然后执行业务,之后将 lockkey 删除,最后将锁释放。如果设置失败,也就是获取锁失败,则直接结束。

这里使用 setnx 命令,开始时 Redis 中不存在 lockkeysetnx(lockkey) 就会返回 1 ,表示本台机器获取到了锁,可以定时执行业务。而其他机器在有效期内获取锁时, lockkey 已经存在,就会返回 0 ,表示没有获取到锁,其他机器正在执行业务。

构建分布式任务调度

利用 Spring Schedule + Redis 分布式锁构建分布式任务调度,方法 closeOrderTaskV2 版本如下:

@Scheduled(cron = "0 */1 * * * ?")
public void closeOrderTaskV2() {
    log.info("关闭订单定时任务启动");
    long lockTimeout = Long.parseLong(PropertiesUtil.getProperty("lock.timeout", "5000"));
    // 获取锁
    Long setnxResult = RedisShardedPoolUtil.setnx(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK, String.valueOf(System.currentTimeMillis() + lockTimeout));
    if (setnxResult != null && setnxResult.intValue() == 1) {
        // 如果返回值是 1,代表设置成功,获取锁
        closeOrder(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
    } else {
        log.info("没有获取到分布式锁:{}", Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
    }
    log.info("关闭订单定时任务结束");
}

private void closeOrder(String lockName) {
    // 修改存活时间
    RedisShardedPoolUtil.expire(lockName, 5);
    log.info("获取{}, ThreadName:{}", Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK, Thread.currentThread().getName());
    int hour = Integer.parseInt(PropertiesUtil.getProperty("close.order.task.time.hour", "2"));
    iOrderService.closeOrder(hour);
    // 删除 key
    RedisShardedPoolUtil.del(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
    log.info("释放{}, ThreadName:{}", Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK, Thread.currentThread().getName());
}
复制代码

缺点

如果某台 tomcat 机器成功获取到锁,但在为锁设置有效期之前, tomcat 机器意外关闭,这时就会产生死锁。

可以在 CloseOrderTask 中添加一个 delLock 方法,在销毁之前删除分布式锁:

@PreDestroy
public void delLock() {
    RedisShardedPoolUtil.del(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
}
复制代码

但如果直接 killtomcat 进程,仍然不会调用这个方法,从而产生死锁。

Redis 分布式锁双重防死锁

Redis 分布式锁优化原理

Redis 分布式锁优化后的流程图如下:

mmall_v2.0 分布式锁实现定时关单

它的原理是:同样首先通过 setnx 存入一个 lockkey ,如果设置成功,同之前一样。否则,通过 get 获得之前设置的 currentTime + timeout ,判断 lockValeA 是否不为 null ,并且 currentTime 大于 lockValueA ,即分布式锁是否过期。

如果过期,通过 getsetlockkey 对应的 value 设置为 currentTime + timeout ,并得到之前的旧值 lockValueB ,判断 lockValueB 是否为 null ,即 lockkey 是否还存在,或者 lockValueA 是否等于 lockValueB ,即在这个过程中锁没有改变。如果条件满足,则表示获取锁成功,同 setnx 获取锁成功一样。

如果锁没有过期,则表示获取锁失败,直接结束。在 getset 后,如果 lockValueB 不为空,即 lockkey 仍然存在,或者锁被改变了,也表示获取锁失败,直接结束。

构建分布式任务调度

利用 Spring Schedule + Redis 分布式锁构建分布式任务调度,方法 closeOrderTaskV3 版本如下:

@Scheduled(cron = "0 */1 * * * ?")
public void closeOrderTaskV3() {
    log.info("关闭订单定时任务启动");
    long lockTimeout = Long.parseLong(PropertiesUtil.getProperty("lock.timeout", "5000"));
    // 获取锁
    Long setnxResult = RedisShardedPoolUtil.setnx(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK, String.valueOf(System.currentTimeMillis() + lockTimeout));
    if (setnxResult != null && setnxResult.intValue() == 1) {
        // 如果返回值是 1,代表设置成功,获取锁
        closeOrder(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
    } else {
        // 未获取到锁,继续判断时间戳,看锁是否过期
        String lockValueStr = RedisShardedPoolUtil.get(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
        if (lockValueStr != null && System.currentTimeMillis() > Long.parseLong(lockValueStr)) {
            // 锁过期,重置并获取锁
            String getSetResult = RedisShardedPoolUtil.getSet(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK, String.valueOf(System.currentTimeMillis() + lockTimeout));
            if (getSetResult == null || (getSetResult != null && StringUtils.equals(lockValueStr, getSetResult))) {
                closeOrder(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
            } else {
                log.info("没有获取到分布式锁:{}", Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
            }
        } else {
            log.info("没有获取到分布式锁:{}", Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
        }
    }

    log.info("关闭订单定时任务结束");
}
复制代码

Redisson 实现分布式锁

Redisson 是架设在 Redis 上的一个 Java 驻内存数据网格,它在基于 NIONetty 框架上,充分地利用了 Redis 键值数据库提供的一系列优势。

Redisson 集成到项目中,只需要在 pom.xml 文件需要添加 redissonjackson-dataformat-avro 的依赖。

Redisson 初始化类

public class RedissonManager {

    private Config config = new Config();
    private Redisson redisson;

    private static String redis1IP = PropertiesUtil.getProperty("redis1.ip", "192.168.23.130");
    private static Integer redis1Port = Integer.parseInt(PropertiesUtil.getProperty("redis1.port", "6379"));

    @PostConstruct
    public void init() {
        try {
            config.useSingleServer().setAddress(redis1IP + ":" + redis1Port);
            redisson = (Redisson) Redisson.create(config);
            log.info("初始化 Redisson 结束");
        } catch (Exception e) {
            log.error("初始化 Redisson 失败", e);
        }
    }

    public Redisson getRedisson() {
        return redisson;
    }
}
复制代码

这里使用单服务器模式,在传入地址时采用 ip:port 格式。

任务调度 v4 版本

tryLock 方法在获取锁时,三个参数分别为:尝试获取锁最多等待的时间、获取锁后自动释放的时间、时间单元。

@Scheduled(cron = "0 */1 * * * ?")
public void closeOrderTaskV4() {
    RLock lock = redissonManager.getRedisson().getLock(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
    boolean locked = false;
    try {
        if(locked = lock.tryLock(0, 5, TimeUnit.SECONDS)) {
            log.info("Redisson 获取到分布式锁:{}, ThreadName:{}", Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK, Thread.currentThread().getName());
            int hour = Integer.parseInt(PropertiesUtil.getProperty("close.order.task.time.hour", "2"));
            iOrderService.closeOrder(hour);
        } else {
            log.info("Redisson 没有获取到分布式锁:{}, ThreadName:{}", Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK, Thread.currentThread().getName());

        }
    } catch (InterruptedException e) {
        log.error("Redisson 获取分布式锁异常", e);
    } finally {
        if (!locked) {
            return;
        }
        lock.unlock();
        log.info("Redisson 释放分布式锁");
    }
}
复制代码

这里 tryLock 方法在获取锁之后,如果后续的执行业务时间小于 1 秒,而另外的 tomcat 在等待 1 秒后,又能重新获取锁,就会出现两个进程都获得锁的情况。

所以,应该将 waitTime 设置为 0waitTime 时间小于业务执行时间)。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Host Your Web Site In The Cloud

Host Your Web Site In The Cloud

Jeff Barr / SitePoint / 2010-9-28 / USD 39.95

Host Your Web Site On The Cloud is the OFFICIAL step-by-step guide to this revolutionary approach to hosting and managing your websites and applications, authored by Amazon's very own Jeffrey Barr. "H......一起来看看 《Host Your Web Site In The Cloud》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具