2.防止惡意刷單
在生產場景下,很有可能會存在某些用戶惡意刷單的情況出現。這樣的操作對於系統而言,會導致業務出錯、髒數據、後端訪問壓力大等問題的出現。
一般要解決這個問題的話,需要前端進行控制,同時後端也需要進行控制。後端實現可以通過Redis incrde 原子性遞增來進行解決。
2.1 更新秒殺服務下單
2.2 防重方法實現
//防止重複提交
private String preventRepeatCommit(String username,Long id) {
String redisKey = "seckill_user_" + username+"_id_"+id;
long count = redisTemplate.opsForValue().increment(redisKey, 1);
if (count == 1){
//設置有效期五分鐘
redisTemplate.expire(redisKey, 5, TimeUnit.MINUTES);
return "success";
}
if (count>1){
return "fail";
}
return "fail";
}
3.防止相同商品重複秒殺
3.1 修改下單業務層實現
3.2 dao層新增查詢方法
public interface SeckillOrderMapper extends Mapper<SeckillOrder> {
/**
* 查詢秒殺訂單信息
* @param username
* @param id
* @return
*/
@Select("select * from tb_seckill_order where user_id=#{username} and seckill_id=#{id}")
SeckillOrder getSecKillOrderByUserNameAndGoodsId(String username, Long id);
}
4.秒殺下單接口隱藏
當前雖然可以確保用戶只有在登錄的情況下才可以進行秒殺下單,但是無法方法有一些惡意的用戶在登錄了之後,猜測秒殺下單的接口地址進行惡意刷單。所以需要對秒殺接口地址進行隱藏。
在用戶每一次點擊搶購的時候,都首先去生成一個隨機數並存入redis,接着用戶攜帶着這個隨機數去訪問秒殺下單,下單接口首先會從redis中獲取該隨機數進行匹配,如果匹配成功,則進行後續下單操作,如果匹配不成功,則認定爲非法訪問。
4.1 將隨機數工具類放入common工程中
package com.changgou.util;
import java.util.Random;
public class RandomUtil {
public static String getRandomString() {
int length = 15;
String base = "abcdefghijklmnopqrstuvwxyz0123456789";
Random random = new Random();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < length; i++) {
int number = random.nextInt(base.length());
sb.append(base.charAt(number));
}
return sb.toString();
}
public static void main(String[] args) {
String randomString = RandomUtil.getRandomString();
System.out.println(randomString);
}
}
4.2 秒殺渲染服務定義隨機數接口
@GetMapping("/getToken")
@ResponseBody
public String getToken(){
String randomString = RandomUtil.getRandomString();
String cookieValue = this.readCookie();
redisTemplate.opsForValue().set("randomcode_"+cookieValue,randomString,5, TimeUnit.SECONDS);
return randomString;
}
private String readCookie(){
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String jti = CookieUtil.readCookie(request, "uid").get("uid");
return jti;
}
4.3 js修改
修改js下單方法
//秒殺下單
add:function(id){
app.msg ='正在下單';
//獲取隨機數
axios.get("/api/wseckillorder/getToken").then(function (response) {
var random=response.data;
axios.get("/api/wseckillorder/add?time="+moment(app.dateMenus[0]).format("YYYYMMDDHH")+"&id="+id+"&random="+random).then(function (response) {
if (response.data.flag){
app.msg='搶單成功,即將進入支付!';
}else{
app.msg='搶單失敗';
}
})
})
}
4.4 秒殺渲染服務更改
修改秒殺渲染服務下單接口
@RequestMapping("/add")
public Result add(@RequestParam("time") String time, @RequestParam("id")Long id,String random){
String cookieValue = this.readCookie();
String redisRandomCode = (String) redisTemplate.opsForValue().get("randomcode_"+cookieValue);
if (StringUtils.isEmpty(redisRandomCode)){
return new Result(false, StatusCode.ERROR,"下單失敗");
}
if (!random.equals(redisRandomCode)){
return new Result(false, StatusCode.ERROR,"下單失敗");
}
Result result = secKillOrderFeign.add(time, id);
return result;
}
5 秒殺下單接口限流
因爲秒殺的特殊業務場景,生產場景下,還有可能要對秒殺下單接口進行訪問流量控制,防止過多的請求進入到後端服務器。對於限流的實現方式,我們之前已經接觸過通過nginx限流,網關限流。但是他們都是對一個大的服務進行訪問限流,如果現在只是要對某一個服務中的接口方法進行限流呢?這裏推薦使用google提供的guava工具包中的RateLimiter進行實現,其內部是基於令牌桶算法進行限流計算
1)添加依賴
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.0-jre</version>
</dependency>
2)自定義限流注解
@Inherited
@Documented
@Target({ElementType.FIELD,ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME) //不僅保存到class文件中,並且jvm加載class之後,該註解仍然存在
public @interface AccessLimit {}
3)自定義切面類
package com.changgou.seckill.web.aspect;
import com.alibaba.fastjson.JSON;
import com.changgou.entity.Result;
import com.changgou.entity.StatusCode;
import com.google.common.util.concurrent.RateLimiter;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
@Scope
@Aspect
public class AccessLimitAop {
@Autowired
private HttpServletResponse response;
//設置令牌的生成速率
private RateLimiter rateLimiter = RateLimiter.create(2.0); //每秒生成兩個令牌存入桶中
@Pointcut("@annotation(com.changgou.seckill.web.aspect.AccessLimit)")
public void limit(){}
@Around("limit()")
public Object around(ProceedingJoinPoint proceedingJoinPoint){
boolean flag = rateLimiter.tryAcquire();
Object obj = null; //返回值
if (flag){
//允許訪問
try {
obj = proceedingJoinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}else{
//不允許訪問,拒絕
String errorMessage = JSON.toJSONString(new Result<>(false, StatusCode.ACCESSERROR,"fail"));
//將信息返回到客戶端上
this.outMessage(response,errorMessage);
}
return obj;
}
private void outMessage(HttpServletResponse response,String errorMessage){
ServletOutputStream outputStream = null;
try {
response.setContentType("application/json;charset=utf-8");
outputStream = response.getOutputStream();
outputStream.write(errorMessage.getBytes("utf-8"));
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
4)使用自定義限流注解
@RequestMapping("/add")
@AccessLimit
public Result add(@RequestParam("time") String time, @RequestParam("id")Long id,String random){
String cookieValue = this.readCookie();
String redisRandomCode = (String) redisTemplate.opsForValue().get("randomcode_"+cookieValue);
if (StringUtils.isEmpty(redisRandomCode)){
return new Result(false, StatusCode.ERROR,"下單失敗");
}
if (!random.equals(redisRandomCode)){
return new Result(false, StatusCode.ERROR,"下單失敗");
}
Result result = secKillOrderFeign.add(time, id);
return result;
}