在我们在tomcat集群下使用Spring Schedule进行定时关单,会多台服务器进行定时关单,我们需要在关单的时候,一台服务器进行关单就可以了,并不需要多台服务器都来执行它,多台服务器一起执行,浪费的mysql和tomcat服务器的性能,因为其他服务器是不需要执行的,只执行一台就行,不然很容易造成数据错乱,因为多台服务器都在执行sql语句,故引出用redis的分布式锁来解决多台服务器同时进行关单操作。
同Spring Schedule类似的有:
1、java原生的:Timer
2、第三方开源框架:Quarter
通过cron来设置定时的时间
工作中使用cron生成器,通过百度搜索 :cron生成器
应用场景:
生成订单后,并没有定时关单,例如下了一个单,30分钟内一直没有付款,那么就把这个订单关闭,然后把下单的产品的库存增加到产品表里面,这样保证产品表的库存又回来了
第一步:在spring.xml配置文件增加配置
<!-- 新增spring schedule的时候新增的,第一个配置是读取 datasource.properties文件的信息-->
<context:property-placeholder location="classpath:datasource.properties"/>
<task:annotation-driven/>
第二步:在java项目task包里面书写CloseOrderTask类,如下:
package com.mmall.task;
import com.mmall.common.Const;
import com.mmall.common.RedissonManager;
import com.mmall.service.IOrderService;
import com.mmall.util.PropertiesUtil;
import com.mmall.util.RedisShardedPoolUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.PreDestroy;
import java.util.concurrent.TimeUnit;
@Component
@Slf4j
public class CloseOrderTask {
@Autowired
private IOrderService iOrderService;
@Autowired
private RedissonManager redissonManager;
@PreDestroy
public void delLock(){
RedisShardedPoolUtil.del(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
}
// @Scheduled(cron="0 */1 * * * ?")//每1分钟(每个1分钟的整数倍)
public void closeOrderTaskV1(){
log.info("关闭订单定时任务启动");
int hour = Integer.parseInt(PropertiesUtil.getProperty("close.order.task.time.hour","2"));
// iOrderService.closeOrder(hour);
log.info("关闭订单定时任务结束");
}
// @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("关闭订单定时任务结束");
}
@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){
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));
//再次用当前时间戳getset。
//返回给定的key的旧值,->旧值判断,是否可以获取锁
//当key没有旧值时,即key不存在时,返回nil ->获取锁
//这里我们set了一个新的value值,获取旧的值。
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("关闭订单定时任务结束");
}
// @Scheduled(cron="0 */1 * * * ?")
public void closeOrderTaskV4(){
RLock lock = redissonManager.getRedisson().getLock(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
boolean getLock = false;
try {
if(getLock = lock.tryLock(0,50, 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(!getLock){
return;
}
lock.unlock();
log.info("Redisson分布式锁释放锁");
}
}
private void closeOrder(String lockName){
RedisShardedPoolUtil.expire(lockName,5);//有效期50秒,防止死锁
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);
RedisShardedPoolUtil.del(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
log.info("释放{},ThreadName:{}",Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK,Thread.currentThread().getName());
log.info("===============================");
}
}
第三步:在订单service接口写方法
//hour个小时以内未付款的订单,进行关闭
void closeOrder(int hour);
第四步:service实现类写方法
@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){
//一定要用主键where条件,防止锁表。同时必须是支持MySQL的InnoDB。
Integer stock = productMapper.selectStockByProductId(orderItem.getProductId());
//考虑到已生成的订单里的商品,被删除的情况
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());
}
}
第五步:在dao层写的方法:
在订单下
//定时关单
List<Order> selectOrderStatusByCreateTime(@Param("status") Integer status,@Param("date") String date);
int closeOrderByOrderId(Integer id);
在产品下
//这里一定要用Integer,因为int无法为NULL,考虑到很多商品已经删除的情况。
Integer selectStockByProductId(Integer id);
第六步:在dao的实现层写的方法:
在订单
<select id="selectOrderStatusByCreateTime" resultMap="BaseResultMap" parameterType="map">
SELECT
<include refid="Base_Column_List"/>
from mmall_order
where status = #{status}
<![CDATA[
and create_time <= #{date}
]]>
order by create_time desc
</select>
<update id="closeOrderByOrderId" parameterType="int" >
UPDATE mmall_order
SET status = 0
where id = #{id}
</update>
【重点】在产品下:(备注:for update表示悲观锁,一定要使用主键,这个锁是一个行锁,如没用主键,则会变成表锁)
<select id="selectStockByProductId" resultType="int" parameterType="java.lang.Integer">
select
stock
from mmall_product
where id = #{id}
for update
</select>