分布式秒杀系统二

分布式秒杀系统二

秒杀场次的获取

public static String getSecKillTime(int i){
        //根据当前时间 计算第一场的时间
        Calendar calendar = Calendar.getInstance();
        //获得当前的小时
        int h = calendar.get(Calendar.HOUR_OF_DAY);
        if(h % 2 != 0){
            h = h - 1;
        }

        calendar.set(Calendar.HOUR_OF_DAY,h);
        calendar.set(Calendar.MINUTE,0);
        calendar.set(Calendar.SECOND,0);
        calendar.set(Calendar.MILLISECOND,0);

        //计算场次
        calendar.add(Calendar.HOUR_OF_DAY,i*2);

        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(calendar.getTime());

    }
@RequestMapping("/times")
    public ResultData<List<String>> getSecKillTime(){

        List<String> times = new ArrayList<>();

        //计算5个场次的时间
        for (int i = 0; i < 5; i++) {
            String time = DateUtil.getSecKillTime(i);
            times.add(time);
        }
        return new ResultData<List<String>>().setData(times);
    }

对秒杀场次进行缓存

添加依赖

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
    </dependencies>

配置redis

spring:
  redis:
    host: 127.0.0.1
    password: 12345
  cache:
    redis:
      #10分钟清空一次缓存
      time-to-live: 600000 

添加注解

该注解表示执行下面方法时先从缓存中查找,如果没有则从数据库中查找,并且插入到redis中(名字通过cacheNames和key进行拼接)

@Cacheable(cacheNames = "seckill",key = "'times'+#time")
public List<Goods> getSecGoodsListByTime(String time) {
    System.out.print("查询数据库了");
    QueryWrapper<Goods>queryWrapper = new QueryWrapper<Goods>().eq("begin_time",time);
    List<Goods> goods = goodsMapper.selectList(queryWrapper);
    return goods;
}

该注解表示每当执行方法都将缓存中的数据库删除,用于增加商品时使用:防止增加完商品后缓存没有变化,进而查商品总从缓存中查找而找不到新增加的商品。

@CachePut(cacheNames="seckill",key="'XXXXXX'")

倒计时的实现

第一版

export default{
		name : "SecKillTime",
		props:['overTime'],
		data(){
			return {
				h:"00",
				m:"00",
				s:"00"
			}
		},
		methods:{
			djs(){
				var _this =this
				//什么时候结束
				var over = new Date(this.overTime);
				//当前时间
				var now = new Date();
				//转换成毫秒值
				var begin  = now.getTime();
				var end = over.getTime();
				//中间差距的时间,相差多少毫秒
				var times = end-begin;
				
				var hour = parseInt(times/1000/60/60);
				var min = parseInt(times/1000/60%60);
				var sec = parseInt(times/1000%60);
				
				this.h = this.formatTime(hour);
				this.m = this.formatTime(min);
				this.s = this.formatTime(sec);
				
				setTimeout(function(){
					// begin-=1000;
					_this.djs();
				},1000);
				
			},
			formatTime(s){
				return s<10?("0"+s):s;
			}
		},
		watch:{
			overTime(v){
				this.djs();
			}
		}
	}

问题

  1. 不能以客户端的时间为准:应该以服务器的时间为准。
  2. 如果服务器集群之后,负载均衡会导致请求分发到不同的集群服务器上,服务器的时间不一致问题:设置一个时间服务器,所有集群服务器写一个脚本,一段时间去获取时间服务器的时间使得误差尽量减少。
  3. 如果要获取服务器的时间,则前端每一秒都要请求一次服务器,服务器压力过大:只在第一次获取服务器时间,后面通过nowtime+=1000人为的设置。

第二版

	var now;
	var begin;
	export default {
		name: "SecKillTime",
		props: ['overTime'],
		data() {
			return {
				h: "00",
				m: "00",
				s: "00"
			}
		},
		methods: {
			djs() {
				var _this = this
				//什么时候结束
				var over = new Date(this.overTime);
				//当前时间
				// var now = new Date();
				//转换成毫秒值
				var end = over.getTime();
				//中间差距的时间,相差多少毫秒
				var times = end - begin;

				var hour = parseInt(times / 1000 / 60 / 60);
				var min = parseInt(times / 1000 / 60 % 60);
				var sec = parseInt(times / 1000 % 60);

				this.h = this.formatTime(hour);
				this.m = this.formatTime(min);
				this.s = this.formatTime(sec);

				setTimeout(function() {
					begin += 1000;
					_this.djs();
				}, 1000);

			},
			formatTime(s) {
				return s < 10 ? ("0" + s) : s;
			},
			getServerNow() {
				var _this = this;
				this.utils.ajax({
					url: this.utils.urls.now,
					success: function(data) {
						now = new Date(data);
						begin = now.getTime();
						_this.djs();
					}
				}, this);
			}
		},
		watch: {
			overTime(v) {
				this.getServerNow();
			}
		}
	}

切换页面时定时器累加问题

//定义一个全局timer
var timer;
//方法中调用setTimeout
timer = setTimeout(function() {
    begin += 1000;
    _this.djs();
}, 1000);
//通过$once来监听定时器,在beforeDestroy钩子可以被清除。
mounted() {
    this.$once('hook:beforeDestroy', () => {
        clearTimeout(timer) // 此处的timer即 上文const的 timer
    })
}

父子组件

父组件

//1. 传递参数和方法
<times :overTime="overTime" :overFunc="overFunc"/>

//2. 导入子组件
import times from "./SecKillTime.vue"
export default {
    name: "SecKillGoods",
    //3. 导入子组件
    components:{
        times
    },
    data() {
        return {
            //4. 传递的数据
            overTime:"",
        }
    },
    methods: {
        //5. 传递的方法(当子组件触发时执行)
        overFunc(){
            this.flag = true;
        }
    }
}

子组件

export default {
    name: "SecKillTime",
    //1. 通过props获取父组件传来的数据和方法
    props: {
        overTime :{
            type:String
        },
        overFunc:{
            type:Function
        }
    },
    methods: {
        // 省略其他
        djs() {
            if(times <= 0 ){
                //2. 调用父组件方法
                this.overFunc();
            }
        }
    }
}

抢购功能实现

如何防止提前下单

  1. 下单前,查询缓存,获取当前商品开始的秒杀时间,和当前时间做一个对比,如果当前时间再秒杀时间之后,则表示可以开启秒杀(会导致redis中的key急剧增加)
  2. 利用redis的HashSet(HashSet的访问速度为O(1))
    1. redis存放一个字符串类型的nowTime(yyMMddHH)。
    2. 并且存放一系列HashSet:key为secKill_{time},value为当前time秒杀场次下的商品列表的id。如key:seckill_20210511,value:876,176,25。
    3. 当请求秒杀某个商品876时,先获取nowTime当前场次,然后和secKill_拼接为key,查看是否存在value为876的值,如果在则可以进行抢购。
    4. 还需要一个定时任务(使用Elastic-Job和Quartz):每两个小时修改redis中的nowTime,并且删除之前场次的集合。

如何防止重复提交

  1. 页面生成时,会生成一个隐藏的UUID号,提交请求后,会带着这个UUID,服务器接收到请求,然后服务器将UUID存入redis,如果redis中已经存在,说明当前请求时重复请求,拒绝处理即可。(只能防止误触碰的重复提交,如果有人恶意的通过工具重复发送请求,没办法判断)
  2. 提交必须输入验证码(好处:1.防止重复提交 2.防止恶意提交,秒杀器,脚本。3.拉长服务器的请求处理时间 坏处:1.普通验证码容易被破解)

版权声明:本文为Ferko原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。