内容简介:在电商项目中,用户购买商品在生成订单后,一定时间内如果没有付款,应该将订单关闭。这里,主要用
在电商项目中,用户购买商品在生成订单后,一定时间内如果没有付款,应该将订单关闭。这里,主要用 Spring Schedule
和分布式锁来实现,而分布式锁也分别用 Redis
命令原生实现和 Redisson
框架两种方式。
Spring Schedule 介绍
Spring Schedule
是一个任务调度框架,用于定时任务调度等。主要通过 @Scheduled
注解来创建定时任务,可通过 cron
表达式来指定任务特定的执行时间。
Cron 表达式
Cron
表达式是一个字符串,由 6
或 7
个字段组成,对应为 秒、分、时、日、月、周、年(可选)。
允许的值和特殊字符
字段名 | 允许的值 | 允许的特殊字符 |
---|---|---|
秒 | 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
,则意味着在5
和20
分各触发一次; -
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("关闭订单定时任务结束"); } } 复制代码
表示每隔一分钟就查看是否有超过一个小时的订单未付款,如果有则进行关闭。 IOrderServiceImpl
的 closeOrder
方法如下:
@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
分布式锁的流程图如下:
它的主要原理是:首先,通过 setnx
存入一个 lockkey
,如果设置成功,也就是获取锁成功,就为锁设置一个有效期,然后执行业务,之后将 lockkey
删除,最后将锁释放。如果设置失败,也就是获取锁失败,则直接结束。
这里使用 setnx
命令,开始时 Redis
中不存在 lockkey
, setnx(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); } 复制代码
但如果直接 kill
掉 tomcat
进程,仍然不会调用这个方法,从而产生死锁。
Redis 分布式锁双重防死锁
Redis 分布式锁优化原理
Redis
分布式锁优化后的流程图如下:
它的原理是:同样首先通过 setnx
存入一个 lockkey
,如果设置成功,同之前一样。否则,通过 get
获得之前设置的 currentTime + timeout
,判断 lockValeA
是否不为 null
,并且 currentTime
大于 lockValueA
,即分布式锁是否过期。
如果过期,通过 getset
将 lockkey
对应的 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
驻内存数据网格,它在基于 NIO
的 Netty
框架上,充分地利用了 Redis
键值数据库提供的一系列优势。
将 Redisson
集成到项目中,只需要在 pom.xml
文件需要添加 redisson
和 jackson-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
设置为 0
( waitTime
时间小于业务执行时间)。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 【分布式锁】07-Zookeeper实现分布式锁:Semaphore、读写锁实现原理
- 原 荐 分布式锁与实现(二)基于ZooKeeper实现
- 分布式实现原理
- 实现分布式锁
- SOFAJRaft 实现原理:SOFAJRaft-RheaKV 分布式锁实现剖析
- RedLock 实现分布式锁
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。