目录
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;
}
}
}
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"
并不全是乐观锁。
行锁与乐观锁相比
乐观锁原理
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会话只有阻塞等待。如果操作不同行,则不会出 -现阻塞情况。防止了数据一致性的问题