秒杀系统的架构设计及实现

秒杀场景在电商平台是十分常见的,这种营销活动往往具有时间短,并发量大的特点。

关于数据库性能

TPS:数据库每秒执行的事务数。
QPS:数据库每秒执行的SQL数。
对于msql数据库,8核CPU16G内存通常TPS:1000 QPS:20000

系统逻辑梳理

用户界面点击请求 ---->服务器收到http请求 ------>修改数据库库存

对于秒杀系统这种短时间的海量请求往往是通过两种思路解决

  1. 分流,将用户操作分散到多CPU处理,例如多线程,负载均衡,集群
  2. 限流,对用户操作进行拦截,缓冲,丢弃处理,例如前端后端都限制用户请求频率,缓存,令牌桶算法

多层次分析架构

用户操作维度:前端页面限制用户请求频率,防止页面重复点击

服务器架设:机器集群,获得更高的的处理能力
负载均衡器nginx
在这里插入图片描述
用户请求先到nginx,再由nginx转发请求到tomcatnginx监听80端口随机转发到tomcat集群下的机器。配置nginx.conf文件如下


#user  nobody;


#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    #并发连接数
    worker_connections  1024;
}


http {
	include mime.types;
	default_type application/octet-stream;

	sendfile on;

	keepalive_timeout 65;

	#tomcat集群
	upstream localhost{ 
	#这里指定多个源服务器,ip:端口,80端口的话可写可不写 
	server 127.0.0.1:8080; 
	server 127.0.0.1:8081; 
	}

	server {
		#监听端口
		listen 80;
		server_name localhost;

		location / {

		#启动代理

			proxy_pass http://localhost;
		}

	}

}

Java程序编码:对于秒杀系统很大的请求量很可能是由脚本发起的频繁请求,所以可以在后台编码中对请求频率进行限制。
1.使用redis存储请求记录,并设置过期时间,当用户请求时,如果redis已经存在则代表短时间请求过,不再让用户请求,直接返回,如果没有则同意用户进行请求。这里setnx+setex要使用组合命令相当于一条命令,用两条命令redis就效率减半了。

/**
     * 如果redis内没有这个<key,value>则插入,否则不插入,这是一个命令
     * 如果是setnx + setex 设置值再设置时间是两条命令。
     * @param key
     * @param value
     * @param expire  有效时间 单位 秒
     * @return
     */
    public Boolean setIfAbsent(String key,String value,long expire){
        Boolean isSuccess = redisTemplate.execute(new RedisCallback<Boolean>() {
            @Override
            public Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException {
                Boolean result = redisConnection.set(key.getBytes(),value.getBytes(),Expiration.seconds(expire),RedisStringCommands.SetOption.SET_IF_ABSENT);
                return result;
            }
        });
        return isSuccess;
    }

controller限制请求

//redis 实现请求频率限制
        boolean result = redisUtil.setIfAbsent(userId,goodsId,5L);
        //没有插入成功,请求频率太快
        if(!result){
            System.out.println("您的频率太快了,请稍后再试");
            return "error";
        }

2.通过上面的频率限制后,真实请求量还是十分庞大,下面使用令牌桶算法进一步限流。
令牌桶:先买票再上车!通过预先(异步)初始化一个和商品数相当数量的令牌池放在内存中,用户先那令牌再去数据库操作。

/**
     * 初始化一个令牌桶
     * @param num
     */
    public void loadTokens(int num){
        //相当一个Queue 从左边压入 右边取出
        for(int i=0;i<num;i++){
            redisTemplate.opsForList().leftPush("token_list",String.valueOf(i));
        }
        System.out.println("令牌桶初始化完毕");
    }

    /**
     * 取一个令牌
     * @return
     */
    public String getToken(){
        String token = (String) redisTemplate.opsForList().rightPop("token_list");
        return token;
    }

在servlet初始化时加载令牌桶

@PostConstruct
    public void init(){
        //实际应通过任务调度 异步加载令牌桶,
        redisUtil.loadTokens(100);
    }
@ResponseBody
    @RequestMapping(value = "/miaosha",method = RequestMethod.POST)
    public String miaosha(String goodsId,String userId){
        //redis 实现请求频率限制
        boolean result = redisUtil.setIfAbsent(userId,goodsId,5L);
        //没有插入成功,请求频率太快
        if(!result){
            System.out.println("您的频率太快了,请稍后再试");
            return "error";
        }

        //令牌桶算法
        //先取得令牌
        String token = redisUtil.getToken();
        if(token == null || "".equals(token)){
            //没有抢到令牌,秒杀失败
            System.out.println(userId+"没有抢到令牌,秒杀失败");
            return "error";
        }

        //秒杀逻辑 消耗资源
        if(orderService.miaoSha(goodsId,userId)){
            return "ok";
        }
        return "error";

    }

总结

根据分流限流的思想对海量请求进行处理
1.前端禁止重复请求 限流
2.负载均衡服务器集群 分流
3.后端禁止快频率请求 限流
4.令牌桶算法 限流

详情代码见秒杀系统

猜你喜欢

转载自blog.csdn.net/weixin_41768073/article/details/84134789
今日推荐