秒杀java代码(乐观锁、行锁防止库存超卖) - 简单版本v1

目录

mapper

需要的两张表 

乐观锁讲解

修改代码为乐观锁

减库存乐观锁、行锁总结


流程:

1.库存>0就减库存,并记录减了库存的用户到抢购成功记录表中,
2.前台定时实时ajax调用接口查询是否抢购成功。

采用乐观锁防止库存<0

乐观锁就是认为不会产生数据访问冲突。比如update修改商品status为2
update 表 set status=2, version=version+1
where id=#{id} and version=#{version};

package com.chuangqi.defense.controller;

import java.util.Date;
import java.util.concurrent.TimeUnit;
import javax.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.chuangqi.defense.mapper.Test_01Mapper;
import lombok.extern.log4j.Log4j2;

/**
 1.抢购:http://127.0.0.1:81/subtractInventory?phone=18713901060&inventoryId=110
 2.查询是否抢购成功:http://127.0.0.1:81/getInventoryLog?phone=18713901060&inventoryId=110
 *
 * @author qizhentao
 * @version V1.0
 */
@Log4j2
@RestController
public class Test_01 {

	@Resource
	private RedisTemplate redisTemplate;
	
	@Autowired
	private Test_01Mapper mapper;
	
    /**
	 * 抢购接口
	 *  1.减库存
	 *  2.记录抢购成功用户
	 * @param inventoryId
	 * @param phone
	 * @return
	 */
	@RequestMapping("subtractInventory")
	public String add(Integer inventoryId, String phone){
		// 1.限制同一用户5秒内只能请求一次,使用setNX命令:已存在返回false,不存在就保存并返回true,超时时间5000毫秒
		Boolean setIfAbsent = redisTemplate.opsForValue().setIfAbsent(phone, inventoryId, 5000, TimeUnit.MILLISECONDS);
		if(!setIfAbsent){
			return "later on...";
		}
		
		// 2.减库存
		int i = mapper.subtractInventory(inventoryId, phone);
		if(i == 1){
			// 3.异步添加用户抢购成功记录表
			addInventoryLog(inventoryId, phone);
			return "ok";
		}else{
			return "no";
		}
	}
	
	/**
	 * 减库存后,添加用户抢购成功记录表中
	 * 	1. 并且添加到redis中,
	 *  2. 在前台实时查询是否抢购成功的时候去查询redis即可
	 * @param inventoryId
	 * @param phone
	 * @return
	 */
	@Async
	public boolean addInventoryLog(int inventoryId, String phone){
		// 1.添加数据到数据库
		int i = mapper.addInventoryLog(inventoryId, phone, new Date());
		// 2.也可同步到redis中
		// redisTemplate.opsForValue().set(phone, inventoryId);
		return i == 1 ? true : false;
	}
	
	/**
	 * 前台定时ajax实时查询是否抢购成功
	 * @param inventoryId
	 * @param phone
	 * @return
	 */
	@RequestMapping("getInventoryLog")
	public String getInventoryLog(int inventoryId, String phone){
		// 查询数据库,也可改为查询redis
		Integer id = mapper.getInventoryLog(inventoryId, phone);
		// redisTemplate.opsForValue().get(inventoryId)
		if(id != null){
			return phone+"用户抢购成功!";
		}else{
			return null;
		}
	}

}

mapper

package com.chuangqi.defense.mapper;

import java.util.Date;

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

public interface Test_01Mapper {
    
	@Update("update inventory set quantity = quantity - 1 where inventoryId = #{inventoryId} and quantity > 0")
	int subtractInventory(@Param("inventoryId")int inventoryId, @Param("phone")String phone);

	@Insert("insert into inventory_log (phone, inventoryId, dateTime) values(#{phone},#{inventoryId},#{date})")
	int addInventoryLog(@Param("inventoryId")int inventoryId, @Param("phone")String phone, @Param("date")Date date);

	@Select("select id from inventory_log where phone = #{phone} and inventoryId = #{inventoryId} limit 1")
	Integer getInventoryLog(@Param("inventoryId")int inventoryId, @Param("phone")String phone);
	
}

需要的两张表 

库存表:
DROP TABLE IF EXISTS `inventory`;
CREATE TABLE `inventory` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `inventoryId` int(11) DEFAULT NULL  COMMENT '库存id',
  `quantity` int(11) DEFAULT NULL COMMENT '库存数量',
  `updateTime` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `deadline` datetime DEFAULT NULL COMMENT '截止时间',
  `insertTime` datetime DEFAULT NULL COMMENT '新增时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;


用户抢购成功记录表:
DROP TABLE IF EXISTS `inventory_log`;
CREATE TABLE `inventory_log` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `phone` varchar(255) DEFAULT NULL COMMENT '用户手机号',
  `inventoryId` int(11) DEFAULT NULL COMMENT '库存id',
  `dateTime` datetime DEFAULT NULL  COMMENT '抢购成功时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1155 DEFAULT CHARSET=utf8;

乐观锁讲解

以上所写的减库存:"update inventory set quantity = quantity - 1 where inventoryId = #{inventoryId} and quantity > 0"

并不全是乐观锁。

秒杀java代码(乐观锁、行锁防止库存超卖) - 简单版本v1_第1张图片

行锁与乐观锁相比

 

秒杀java代码(乐观锁、行锁防止库存超卖) - 简单版本v1_第2张图片

乐观锁原理

秒杀java代码(乐观锁、行锁防止库存超卖) - 简单版本v1_第3张图片


修改代码为乐观锁

1. inventory库存表添加添加version版本号字段

2. 修改库存sql语句

package com.chuangqi.defense.mapper;

import java.util.Date;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

public interface Test_01Mapper {
    
		// 行锁减库存
	@Update("update inventory set quantity = quantity - 1 where inventoryId = #{inventoryId} and quantity > 0")
	int subtractInventory(@Param("inventoryId")int inventoryId);

	// 乐观锁减库存
	@Update("UPDATE inventory SET quantity = quantity - 1, version = version + 1 WHERE inventoryId = #{inventoryId} AND quantity > 0 AND version = #{version}")
	int subtractInventoryOptimisticByVersion(@Param("inventoryId")int inventoryId, @Param("version")Integer version);
	
	// 用户抢购成功,记录到抢购成功记录表
	@Insert("insert into inventory_log (phone, inventoryId, dateTime) values(#{phone},#{inventoryId},#{date})")
	int addInventoryLog(@Param("inventoryId")int inventoryId, @Param("phone")String phone, @Param("date")Date date);

	// 查询用户是否抢购成功
	@Select("select id from inventory_log where phone = #{phone} and inventoryId = #{inventoryId} limit 1")
	Integer getInventoryLog(@Param("inventoryId")int inventoryId, @Param("phone")String phone);

	// 查询商品库存是否存在,返回版本号
	@Select("select version from inventory where inventoryId = #{inventoryId} limit 1")
	Integer getInventory(@Param("inventoryId")Integer inventoryId);
}

-- 分解乐观锁
-- 1.根据库存id获取版本值,version返回4
SELECT b.version FROM    inventory b WHERE b.inventoryId = #{库存Id}
-- 2.版本值作为条件进行乐观锁(无锁)进行update
UPDATE inventory SET quantity = quantity - 1, version = a.version + 1 WHERE inventoryId = #{库存Id} AND quantity > 0 AND a.version = 4

3. 业务层修改

   3.1.根据库存id查询商品库存是否存在,返回版本号  

   3.2.调用减库存方法,改为使用乐观锁的减库存的sql:Test_01Mapper.subtractInventoryOptimisticByVersion()接口方法。

减库存乐观锁、行锁总结

// 如果采用提前生成库存数量保存到redis后,就没必要使用乐观锁了,直接使用行锁机制就行
// 因为:能在redis抢到令牌的,就已经代表抢到了商品,所以使用乐观锁和行锁是没什么区别的
// 乐观锁:如果有100库存,同时来一百线程,有的线程会查询到相同的版本号,所以不会一次减完100个库存。
// 行锁:两个线程操作同一行数据,由于InnoDB为行锁,在A会话未提交时,B会话只有阻塞等待。如果操作不同行,则不会出 -现阻塞情况。防止了数据一致性的问题

你可能感兴趣的:(秒杀系统设计)