Giter Site home page Giter Site 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

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  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.