Giter Club home page Giter Club logo

seckilldesignimpl's Introduction

SeckillDesignImpl

高并发秒杀系统设计与实现

开发环境

eclipse + tomcat8 + jdk8

使用技术

Spring + Spring MVC + Redis + MySQL

技术选型:为什么选择Redis?

  • 数据类型丰富(string,hash,list,set,sorted list),可根据业务选择不同的数据类型;
  • 单线程,不存在锁竞争现象,实现相对容易,代码简洁,可读性强;
  • 可持久化,redis提供了两种持久化方式RDB,AOF,可防止数据丢失
  • 高可用(从后期业务扩展角度考虑),redis支持集群功能,可以实现主从复制,读写分离,提供哨兵机制等等

详细可参考这篇文章:技术选型:redis与memcache

内容概述

对高并发业务场景进行分析,以解决秒杀系统核心问题为目的,设计一个秒杀系统,分析秒杀业务场景,提供一种可行的解决方案,达到限流、分流效果,进而提升系统性能:

  • 问题1:系统超卖,导致亏本,解决策略为:使用数据库行级锁,实现同步执行效果。
  • 问题2:用户重复点击,请求量大于用户量,解决策略为:通过前端控制(按钮禁用,button.disabled),防止重复点击。
  • 问题3:单机性能瓶颈:单机服务器处理能力有限,解决策略为:使用Nginx负载均衡,通过upstream模块实现多个Tomcat的负载均衡。
  • 问题4:非普通用户(程序员)使用脚本工具进行抢购,解决策略为:使用Redis中的key的有效期(EX)和NX机制,限制单个用户的操作频率。
  • 问题5:数据库连接请求量巨大,存在性能瓶颈,解决策略为:使用Redis的队列实现令牌池,令牌数量有限,先到先得,成功获取令牌的允许请求数据库连接。

扩展(未实现):

  • 单台Redis性能问题:用户量多时,并发量会更高,超出单台Redis吞吐量,解决方法是使用Redis集群,实现高可用
  • 数据库性能瓶颈:当商品库存很多,采取上述多策略后,数据库请求依然多,压力依然大,解决方法是数据库分库分表、通过消息队列(如:Kafka、RabbitMQ等)异步执行SQL。
  • 系统架构问题:秒杀系统与业务系统混叠,互相影响导致不可用,秒杀场景流量突发,需要支持快速扩容,解决方法是使用微服务架构(Spring cloud、dubbo),独立秒杀系统;docker容器,实现快速扩容。

业务场景

1、用户界面:用户点击秒杀按钮

2、Web程序服务器:服务器收到http请求,修改数据

3、数据库:修改商品余量,添加秒杀成功记录

系统设计

数据库表设计

设计两个简单的表:秒杀记录表和秒杀商品表,sql实现如下:

create table 'tb_records' (
	'records_id' int(11) not null auto_increment,
	'goods_code' varchar(11) default null comment '商品唯一编码',
	'user_id' varchar(255) default null comment '用户唯一ID',
	primary key ('records_id')
)engine = InnoDB auto_increment = 1031 default charset = latinl comment = '秒杀记录表';
create table 'tb_seckil(
	'goods_code' varchar(11) not null comment '商品唯一编码',
	'goods_nums' int(255) default null comment '商品余量',
	primary key ('goods_code')
)engine = InnoDB default charset = latinl comment = '秒杀商品表';

核心类设计

业务处理接口:

/**
* description:业务层,处理秒杀业务逻辑处理
* @author vechace
* date 2018/6/15
*/
@Service
public class SeckillService {
	private final Logger logger = Logger.getLogger(SeckillService.class);
	@Autowired
	RedisCacheUtils redisPool;
	@Autowired
	SeckillDao seckillDao;
	/**
	 * 秒杀业务层接口
	 * @param goodsCode
	 * @param userId
	 * @return
	 */
	public boolean seckill(String goodsCode,String userId){
		/**
		 * 限制用户的操作频率,10s操作一次
		 * 使用组合指令:SET key value EX 5 NX
		 * 细节:不能拆分成两条指令: SET key EX ;SET key NX,会导致性能下降一半
		 */
		Jedis jedis = redisPool.getJedis();
		String value = jedis.set(userId, "","NX","EX",10);
		if(!"OK".equals(value)){
			logger.warn("被限制操作频率,用户: " +userId);
			return false;
		}
		/**
		 * token令牌机制:采用Redis的列表类型实现令牌队列,
		 * lpop指令从队列左边弹出一个令牌,先到先得,拿到令牌的可以请求数据库
		 */
		String token = jedis.lpop("token_list");
		if(token == null){
			logger.warn("没有抢到token令牌,不参与秒杀,用户: " + userId);
			return false;
		}
		boolean result = seckillDao.buy(goodsCode, userId);
		logger.warn("秒杀结果: " +result);
		return result;
	}
}

数据更新接口:

/**
* description:数据层,请求数据库并更新记录
* @author vechace
* date 2018/6/15
*/
@Repository
public class SeckillDao {
	//使用Spring jdbc模板
	@Autowired
	JdbcTemplate jdbcTemplate;
	//事务回滚则抛出异常
	@Transactional(rollbackFor = Exception.class)
	public boolean buy(String goodsCode,String userId){
		//商品数量减1
		String sql = "update tb_seckill set goods_nums = goods_nums-1 where goods_code = '"+ goodsCode +"' and goods_nums-1 >=0 ";
		int count = jdbcTemplate.update(sql);
		if(count !=1){
			return false;
		}
		//秒杀成功则,添加购买记录
		String insertSql = "insert into tb_records(goods_code,user_id) value('" + goodsCode +"','" + userId + "')";
		int insertCount = jdbcTemplate.update(insertSql);
		if(insertCount !=1){
			return false;
		}
		return true;
	}
}

系统整体架构

架构

系统分析

问题1(核心问题):超卖问题,导致亏本

解决策略:借助数据库锁机制,防止超卖:多个请求并发操作同一行记录时,数据库会锁住该行记录(行级锁),实现同步执行的效果。需要注意的是:MySQL存在各种锁(行级锁、表级锁、页级锁等),各个存储引擎的支持不同,如下:

  • MyISAM仅支持表级锁
  • BDB支持表级锁和页级锁
  • InnoDB支持行级锁和表级锁

其他可行策略:

  • MySQL添加字段version(版本号),实现MVCC乐观锁
  • 基于AtomicInteger的CAS机制,即使用JVM锁机制
  • 使用Redis作为原子计数器(watch事务,decr操作),kafka作为消息队列记录用户抢购行为

问题2:用户重复点击,请求量大于用户量

解决策略:针对普通用户,可以通过前端控制(按钮置灰,butter.disable),防止重复点击,可减少90%的重复请求。

//控制访问频率,disable按钮置灰,禁用
$("#test").attr('disabled',true);
//5秒后自动启用
setTimeout(function(){
	$("#test").removeAttr("disabled");
},5000);

问题3:单机性能瓶颈:单机服务器能处理的并发量有限

解决策略:负载均衡(软件方式),使用Nginx是最常用的高性能web服务器,通过upstream模块实现多个Tomcat的负载均衡。

其他实现方式:LVS(第四层负载均衡)、F5、Radware(硬件负载均衡)

架构

Nginx配置

events{
	#并发连接数
    worker_connections 1024;
}
http{
    #Tomcat服务器集群,这里假设有两个tomcat服务器实现分流
    upstream tomcat_servers{
        server 127.0.0.1:8081;
        server 127.0.0.1:8080;
    }
    server{
        #监听80端口
        listen 80;
        #将所有请求交给Tomcat集群去处理
        location / {
            proxy_pass http://tomcat_servers;
        }
    }
}

集群存在问题:

  • 分布式session:使用Redis缓存用户session,实现分布式session一致性
  • 分布式事务:分布式锁

问题4:非普通用户(程序员)使用脚本工具进行抢购

解决策略:针对非普通用户,限制单个用户的操作频率,通过Redis中的key的有效期(EX)和NX机制实现,组合命令:

set key value EX 5 NX

原理:如果Redis中没有这个key,则设置成功,过期时间10秒,如果Redis已经有这个key,代表10秒内已经操作过1次

java实现:

/**
 * 限制用户的操作频率,10s操作一次
 * 使用组合指令:SET key value EX 5 NX
 * 细节:不能拆分成两条指令: SET key EX ;SET key NX,会导致性能下降一半
 */
	Jedis jedis = redisPool.getJedis();
	String value = jedis.set(userId, "","NX","EX",10);
	if(!"OK".equals(value)){
		logger.warn("被限制操作频率,用户: " +userId);
		return false;
	}

细节分析:推荐使用组合指令,而不使用两条指令,因为根据Redis压力测试报告可知,当使用2条set指令时,系统性能可能会下降一半(高并发场景下)。

$ redis-benchmark -t set,lpush -n 100000 -q
SET: 74239.05 requests per second
LPUSH: 79239.30 requests per second

redis压力测试报告:how fast is redis?

问题5:数据库请求量巨大

解决策略:数据库操作成功的量大于或等于商品数量:用户请求量大于等于用户数,而数据库操作成功次数小于等于秒杀商品数量。商品数量有限,能秒杀成功的请求极少。可采用令牌机制解决问题

令牌机制:通过预先(异步)初始化一个和商品数量相当的令牌池放在内存中,用户请求到来时,去内存中取令牌,令牌数量有限,先到先得,拿到令牌的可以请求数据库,从而减少数据库的压力。先到先得,拿到令牌的去秒杀,没拿到的回绝。

使用Redis实现令牌池

@Before
public void start(){
	System.out.println("开始测试");
	//初始化token令牌池,使用lpush指令,从token_list左边push,队列值为0-100
	Jedis jedis = new Jedis();
	jedis.del("token_list");
	for(int i = 0;i<100;i++){
		jedis.lpush("token_list", String.valueOf(i));
	}
	jedis.close();
	System.out.println("100个令牌池初始化完成");
	System.out.println("继续测试");
}

秒杀方法:

/**
 * token令牌机制:采用Redis的列表类型实现令牌队列,
 * lpop指令从队列左边弹出一个令牌,先到先得,拿到令牌的可以请求数据库
 */
String token = jedis.lpop("token_list");
if(token == null){
	logger.warn("没有抢到token令牌,不参与秒杀,用户: " + userId);
	return false;
}

高并发系统

  • 限流:前端限制(按钮置灰)、服务端限流(限制用户请求次数)、令牌池
  • 分流:负载均衡、分表、分库、消息队列

方案总结

策略思路:分析业务场景,从业务角度去解决存在问题或提升系统性能,可以适当跳出常规的思维(从技术角度去想),实际过程中,通过分析秒杀系统可能存在的场景,得出方案,从前端控制,到服务端限流分流,再到数据库限流,逐步设计出具体方案,总结如下:

  • 策略1:数据库乐观锁机制,防止超卖(核心问题)
  • 策略2:前端防止重复提交
  • 策略3:服务端做负载均衡
  • 策略4:后台限制用户操作频率
  • 策略5:令牌发放机制

拓展策略:当业务量持续增大时,可采用如下方案:

  • 单台Redis不够用:用户量多,并发量更高,超出单台Redis吞吐量,解决方法是使用Redis集群、高可用。
  • 数据库压力大:当商品库存很多,采取上述多策略后,数据库请求依然多,压力依然大,解决方法是数据库分库分表、通过消息队列异步执行SQL。
  • 系统架构:与业务系统混合在一起,互相影响,导致不可用,而秒杀场景的突发流量,要支持快速扩容,解决方法是微服务架构,独立秒杀系统;docker容器化技术,快速扩容。

备注:学习源自网络,并加以总结。

参考资料:《深入理解Nginx》、《Redis 开发与运维》、Redis官方文档、Redis压测报告、Java8开发文档(多线程)等等

seckilldesignimpl's People

Contributors

angelswen avatar

Watchers

James Cloos avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.