内容简介:在电商项目中,用户购买商品在生成订单后,一定时间内如果没有付款,应该将订单关闭。这里,主要用
在电商项目中,用户购买商品在生成订单后,一定时间内如果没有付款,应该将订单关闭。这里,主要用 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 实现分布式锁
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
ACM国际大学生程序设计竞赛亚洲区预选赛真题题解
郭炜 / 电子工业 / 2011-7 / 49.00元
ACM国际大学生程序设计竞赛(ACM International Collegiate Programming Contest,简称ACM/ICPC)是世界上历史最悠久,规模最大、最具声望的程序设计竞赛,一直受到众多国际知名大学的重视,全球著名IT公司更是争相招募竞赛的优胜者。 该项赛事分为各大洲预选赛和全球总决赛两个阶段。北京大学多次在亚洲区预选赛中负责命题工作,是中国在ACM/ICPC命......一起来看看 《ACM国际大学生程序设计竞赛亚洲区预选赛真题题解》 这本书的介绍吧!