搭建基础环境
entity
Order.java
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Accessors(chain = true)
public class Order {
private Integer id;
private Integer sid;
private String name;
private Date createDate;
}
Stock.java
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Accessors(chain = true)
public class Stock {
private Integer id;
private String name;
private Integer count;
private Integer sale;
private Integer version;
}
dao
OrderDAO.java
@Repository
public interface OrderDAO {
//创建订单方法
void createOrder(Order order);
}
StockDAO.java
@Repository
public interface StockDAO {
//根据商品Id查询库存信息的方法
Stock checkStock(Integer id);
//根据ID扣除库存
void updateSale(Stock stock);
}
service
OrderService.java
public interface OrderService {
//用来处理秒杀的下单方法,并返回订单id
int kill(Integer id);
}
OrderServiceImpl.java
@Service
@Transactional
public class OrderServiceImpl implements OrderService {
@Autowired
private StockDAO stockDAO;
@Autowired
private OrderDAO orderDAO;
@Override
public int kill(Integer id) {
//根据商品id校验库存
Stock stock = stockDAO.checkStock(id);
if (stock.getSale().equals(stock.getCount())){
throw new RuntimeException("库存不足!!!");
}else{
//扣除库存
stock.setSale(stock.getSale()+1);
stockDAO.updateSale(stock);
//创建订单
Order order = new Order();
order.setSid(stock.getId()).setName(stock.getName()).setCreateDate(new Date());
orderDAO.createOrder(order);
return order.getId();
}
}
}
mapper
OrderDAOMapper.xml
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.axiang.dao.OrderDAO">
<!--创建订单-->
<insert id="createOrder" parameterType="Order" useGeneratedKeys="true" keyProperty="id">
insert into stock_order values(#{id},#{name},#{sid},#{createDate})
</insert>
</mapper>
StockDAOMapper.xml
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.axiang.dao.StockDAO">
<!-- 根据id查询库存-->
<select id="checkStock" parameterType="int" resultType="Stock">
select id,name,count,sale,version from stock
where id = #{id}
</select>
<update id="updateSale" parameterType="Stock">
update stock set sale = #{sale} where id = #{id}
</update>
</mapper>
搭建高并发测试环境
去https://jmeter.apache.org/download_jmeter.cgi下载jmeter
解压+配置环境变量
GUI运行:在bin下双击jmeter.bat运行(不推荐)
命令行运行:
在bin下cmd;
输入命令:
jmeter -n -t C:\Users\Administrator\Desktop\miaosha.jmx -l C:\Users\Administrator\Desktop\miaosha.txt -e -o Desktop.html
悲观锁解决超卖
在方法处加上synchronized关键字
比库存超出一点
原因:
事务的线程范围比synchronized大:synchronized的代码块结束后,事务还没有结束,但是锁已经释放了且下一个线程来了,又把下一个线程和数据库提交了。所以会产生多提交的问题。
事务注解加方法上就没事了,一般也不会加在类上面
解决:
- 把事务注解去掉(但是库存不足时需要事务控制程序不再创建订单)
- 把同步代码块放在控制器调用处:(使线程(synchronized)的执行范围比事务大)
乐观锁解决超卖
优化代码OrderService.java
@Service
@Transactional
public class OrderServiceImpl implements OrderService {
@Autowired
private StockDAO stockDAO;
@Autowired
private OrderDAO orderDAO;
@Override
public int kill(Integer id) {
//校验库存
Stock stock = checkStock(id);
//更新库存
updateSale(stock);
//创建订单
return createOrder(stock);
}
//校验库存
private Stock checkStock(Integer id){
Stock stock = stockDAO.checkStock(id);
if (stock.getSale().equals(stock.getCount())){
throw new RuntimeException("库存不足!!!");
}
return stock;
}
//扣除库存
private void updateSale(Stock stock){
stock.setSale(stock.getSale()+1);
stockDAO.updateSale(stock);
}
//创建订单
private Integer createOrder(Stock stock){
Order order = new Order();
order.setSid(stock.getId()).setName(stock.getName()).setCreateDate(new Date());
orderDAO.createOrder(order);
return order.getId();
}
}
使用乐观锁实际上是把防止超卖问题交给数据库解决,利用数据库中定义的 version字段
以及数据库中的事务
现在并发情况下商品的超卖问题
1.更新库存方法改造
//扣除库存
private void updateSale(Stock stock){
//在sql层面完成销量的+1 和版本号的 +1 并且根据商品id和版本号同时查询更新的商品
stockDAO.updateSale(stock);
}
<update id="updateSale" parameterType="Stock">
update stock set
sale = sale + 1,
version = version + 1
where
id = #{id}
and
version = #{version}
</update>
2.再次优化库存方法
//扣除库存
private void updateSale(Stock stock){
//在sql层面完成销量的+1 和版本号的 +1 并且根据商品id和版本号同时查询更新的商品
int updateRows = stockDAO.updateSale(stock);
if (updateRows == 0){
throw new RuntimeException("抢购失败,请重试!!!");
}
}
令牌桶介绍
//创建令牌桶实例
private RateLimiter rateLimiter = RateLimiter.create(40);
@GetMapping("sale")
public String sale(Integer id){
//1.没有获取到token请求,直到获取到token令牌
// log.info("等待时间:" +rateLimiter.acquire());
//2.设置一个等待时间,如果在等待时间内获取到token令牌,则处理业务,若在等待时间内没有获取到响应token则抛弃
if (!rateLimiter.tryAcquire(5, TimeUnit.SECONDS)){
System.out.println("当前请求被限流,直接抛弃,无法调用后续秒杀逻辑...");
return "抢购失败";
}
System.out.println("处理业务...............");
return "抢购成功";
}
令牌桶接口限流
1.导入RateLimter依赖
<!-- google开源工具类RateLimter令牌桶实现-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.2-jre</version>
</dependency>
2.在控制类加入令牌桶功能
一次令牌桶生成20个令牌,在高并发请求中,没有拿到令牌的请求只等待两秒,若超时就抛弃请求
//创建令牌桶实例
private RateLimiter rateLimiter = RateLimiter.create(20);
//乐观锁防止超卖+令牌桶限流
@GetMapping("killtoken")
public String killtoken(Integer id){
System.out.println("秒杀商品的id = " + id);
//加入令牌桶的限流措施
if (!rateLimiter.tryAcquire(2,TimeUnit.SECONDS)){
log.info("抛弃请求:抢购失败,当前秒杀活动过于火爆,请重试");
return "抢购失败,当前秒杀活动过于火爆,请重试";
}
try {
//根据秒杀商品id 去调用秒杀业务
int orderId = orderService.kill(id);
return "秒杀成功,订单id为: "+String.valueOf(orderId);
}catch (Exception e){
e.printStackTrace();
return e.getMessage();
}
}
redis设置秒杀时间
1.导入redis依赖
<!-- redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.在配置文件中配置redis
spring.redis.database=0
spring.redis.port=6379
spring.redis.host=localhost
3.在校验库存之前校验redis中秒杀商品是否超时
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public int kill(Integer id) {
//校验redis中秒杀商品是否超时
if (!stringRedisTemplate.hasKey("kill"+id))
throw new RuntimeException("当前商品的抢购活动已经结束啦~~");
//校验库存
Stock stock = checkStock(id);
//更新库存
updateSale(stock);
//创建订单
return createOrder(stock);
}
//校验库存
private Stock checkStock(Integer id){
Stock stock = stockDAO.checkStock(id);
if (stock.getSale().equals(stock.getCount())){
throw new RuntimeException("库存不足!!!");
}
return stock;
}
接口隐藏
1.创建User实体类、dao类、mapper.xml
并在实现类导入UserDAO
@Data
public class User {
private Integer id;
private String name;
private String password;
}
@Repository
public interface UserDAO {
User findById(Integer id);
}
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.axiang.dao.UserDAO">
<!-- 根据id查询用户-->
<select id="findById" parameterType="Integer" resultType="User">
select id,name,password from user where id = #{id}
</select>
</mapper>
2.数据库建user表
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS =0;
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`(
`id` int(11) not null auto_increment comment '主键',
`name` varchar(80) default null comment '用户名',
`password` varchar(40) default null comment '用户密码',
primary key (`id`)
)engine = innodb auto_increment=2 default charset=utf8;
set foreign_key_checks = 1;
添加一个用户数据
3.在实现类编写getMd5方法
@Override
public String getMd5(Integer id, Integer userid) {
//检验用户的合法性
User user = userDAO.findById(userid);
if (user == null)throw new RuntimeException("用户信息不存在!");
log.info("用户信息:[{}]",user.toString());
//检验商品的合法性
Stock stock = stockDAO.checkStock(id);
if (stock == null)throw new RuntimeException("商品信息不合法!");
log.info("商品信息:[{}]",stock.toString());
//生成hashKey
String hashKey = "KEY_"+userid+"_"+id;
//生成md5;这里的 !Q*jS# 是一个盐 随机生成
String key = DigestUtils.md5DigestAsHex((userid+id+"!Q*jS#").getBytes());
stringRedisTemplate.opsForValue().set(hashKey,key,120, TimeUnit.SECONDS);
log.info("Redis写入:[{}] [{}]",hashKey,key);
return key;
}
4.在控制类添加生成md5的方法
//生成md5值的方法
@RequestMapping("md5")
public String getMd5(Integer id,Integer userid){
String md5;
try{
md5 = orderService.getMd5(id,userid);
}catch (Exception e){
e.printStackTrace();
return "获取md5失败:"+e.getMessage();
}
return "获取md5信息为:"+md5;
}
5.在实现类加入验证签名的方法
@Override
public int kill(Integer id, Integer userid, String md5) {
// //校验redis中秒杀商品是否超时
// if (!stringRedisTemplate.hasKey("kill"+id))
// throw new RuntimeException("当前商品的抢购活动已经结束啦~~");
//验证签名
String hashKey = "KEY_"+userid+"_"+id;
String s = stringRedisTemplate.opsForValue().get(hashKey);
if (s==null) throw new RuntimeException("没有携带验证签名,请求不合法!");
if (!s.equals(md5)) throw new RuntimeException("当前请求数据不合法,请稍后再试");
//校验库存
Stock stock = checkStock(id);
//更新库存
updateSale(stock);
//创建订单
return createOrder(stock);
}
6.在控制类调用加入验证签名后的秒杀方法
//创建令牌桶实例
private RateLimiter rateLimiter = RateLimiter.create(20);
//乐观锁防止超卖+令牌桶限流
@GetMapping("killtokenmd5")
public String killtoken(Integer id,Integer userid,String md5){
System.out.println("秒杀商品的id = " + id);
//加入令牌桶的限流措施
if (!rateLimiter.tryAcquire(3,TimeUnit.SECONDS)){
log.info("抛弃请求:抢购失败,当前秒杀活动过于火爆,请重试");
return "抢购失败,当前秒杀活动过于火爆,请重试";
}
try {
//根据秒杀商品id 去调用秒杀业务,并验证签名
int orderId = orderService.kill(id,userid,md5);
return "秒杀成功,订单id为: "+String.valueOf(orderId);
}catch (Exception e){
e.printStackTrace();
return e.getMessage();
}
}
先要证明用户登录,之后再限制用户的请求频率,就可以限制接口访问压力了
对单个用户频率限制
1.编写UserService
public interface UserService {
//向redis中写入用户访问次数
int saveUserCount(Integer userId);
//判断单位时间调用次数
boolean getUserCount(Integer userId);
}
2.编写UserServiceImpl
@Service
@Transactional
@Slf4j
public class UserServiceImpl implements UserService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public int saveUserCount(Integer userId) {
//根据不同用户id生成调用次数的key
String limitKey = "LIMIT"+"_"+userId;
//获取redis中指定key的调用次数
String limitNum = stringRedisTemplate.opsForValue().get(limitKey);
int limit = -1;
if (limitNum == null){
//第一次调用放入redis中设置为0
stringRedisTemplate.opsForValue().set(limitKey,"0",3600, TimeUnit.SECONDS);
}else{
//不是第一次调用每次+1
limit = Integer.parseInt(limitNum)+1;
stringRedisTemplate.opsForValue().set(limitKey,String.valueOf(limit),3600,TimeUnit.SECONDS);
}
return limit;
}
@Override
public boolean getUserCount(Integer userId) {
//根据userId对应的key获取调用次数
String limitKey = "LIMIT"+"_"+userId;
//跟库用户调用次数的key获取redis中调用次数
String limitNum = stringRedisTemplate.opsForValue().get(limitKey);
if (limitNum == null){
//为空直接抛弃说明key出现异常
log.error("该用户没有访问申请验证值记录,疑似异常");
return true;
}
return Integer.parseInt(limitNum)>10;//false代表没有超过 true代表超过
}
}
3.在控制类编写加入单用户访问频率限制
//乐观锁防止超卖+令牌桶限流+md5签名(hash接口隐藏)+单用户访问频率限制
@GetMapping("killtokenmd5limit")
public String killtokenlimit(Integer id,Integer userid,String md5){
//加入令牌桶的限流措施
if (!rateLimiter.tryAcquire(3,TimeUnit.SECONDS)){
log.info("抛弃请求:抢购失败,当前秒杀活动过于火爆,请重试");
return "抢购失败,当前秒杀活动过于火爆,请重试";
}
try {
//单用户调用接口的频率限制
int count = userService.saveUserCount(userid);
log.info("用户截至该次的访问次数为:[{}]",count);
//进行调用次数判断
boolean isBanned = userService.getUserCount(userid);
if (isBanned){
log.info("购买失败,超过频率限制!");
return "购买失败,超过频率限制!";
}
//根据秒杀商品id 去调用秒杀业务,并验证签名
int orderId = orderService.kill(id,userid,md5);
return "秒杀成功,订单id为: "+String.valueOf(orderId);
}catch (Exception e){
e.printStackTrace();
return e.getMessage();
}
}