SSM项目秒杀系统---(四)高并发优化

一、秒杀系统高并发优化分析

下图中显示的红色部分都涉及到高并发问题。
在这里插入图片描述
1、为什么要单独获取系统时间
在这里插入图片描述
系统的详情页实际上部署在CDN节点上,会对详情页进行静态处理,此时详情页不在我们的系统上,所以系统的时间需要单独做一个请求获得,不能直接在详情页中获得。
(1)CDN的理解
CDN(内容分发网络)加速用户获取数据的系统,部署在离用户最近的网络节点上,命中CDN不需要访问后端服务器,互联网公司自己搭建或租用。
(2)获取时间不用优化
访问一次内存大约10ns,1s中可以完成1亿次,可以不用优化。
2、秒杀地址接口分析
因为秒杀地址随着时间会变化,所以无法使用CDN缓存,适合服务端缓存(redis等),一致性维护成本低。
秒杀地址接口优化:
在这里插入图片描述
3、秒杀操作优化分析
无法使用CDN缓存,后端缓存困难(库存问题),一行数据竞争(热点商品)。
(1)其他方案分析
在这里插入图片描述
这个架构能抗住很高的并发。
成本分析:运维成本和稳定性(NoSQL、MQ等)、开发成本(数据一致性、回滚方案等)、幂等性难保证(重复秒杀问题)、不适合新手的架构。
(2)为什么不用MySQL来解决
一条update压力测试约4wQPS。
java控制事务行为分析:update减库存—insert购买明细—commit/rollback,其他update语句则等待行锁,直到commit后获得锁lock。串行化的操作,容易产生阻塞。
瓶颈分析:update减库存—insert购买明细—commit/rollback的过程中都会出现网络延迟或GC。
优化分析:行级锁在commit之后释放修改为减少行级锁持有时间。
(3)如何判断update更新库存成功
两个条件:①update本身没报错;②客户端确认update影响记录数。
优化思路:把客户端逻辑放到MySQL服务端,避免网络延迟和GC影响。
(4)如何放到MySQL服务端
使用存储过程:整个事务在MySQL端完成。
(5)优化总结
前端控制:暴露接口、按钮防重复
动静态数据分离:CDN缓存、后端缓存
事务竞争优化:减少事务锁时间

二、redis后端缓存优化编码

1、使用redis暴露地址接口

去redis下载最新版本,然后解压进入使用make、make install。
redis-server开启redis服务。
redis-cli在新的终端中连接redis。
在pom.xml中添加java连接redis客户端Jedis依赖:

<!--引入java-redis客户端:Jedis-->
<dependency>
  <groupId>redis.clients</groupId>
  <artifactId>jedis</artifactId>
  <version>3.1.0</version>
</dependency>
<!--redis过程中需要自定义序列化的相关依赖-->
<dependency>
  <groupId>com.dyuproject.protostuff</groupId>
  <artifactId>protostuff-core</artifactId>
  <version>1.1.5</version>
</dependency>
<dependency>
  <groupId>com.dyuproject.protostuff</groupId>
  <artifactId>protostuff-runtime</artifactId>
  <version>1.1.5</version>
</dependency>

在dao/cache下新增RedisDao.java

package org.luyangsiyi.seckill.dao.cache;

import com.dyuproject.protostuff.LinkedBuffer;
import com.dyuproject.protostuff.ProtostuffIOUtil;
import com.dyuproject.protostuff.runtime.RuntimeSchema;
import org.luyangsiyi.seckill.entity.Seckill;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

/**
 * Created by luyangsiyi on 2020/2/24
 */
public class RedisDao {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    private JedisPool jedisPool;

    public RedisDao(String ip, int port){
        jedisPool = new JedisPool(ip,port);
    }

    private RuntimeSchema<Seckill> schema = RuntimeSchema.createFrom(Seckill.class);

    public Seckill getSeckill(long seckillId){
        //redis操作逻辑
        try{
            Jedis jedis = jedisPool.getResource();
            try{
                String key = "seckill:"+seckillId;
                //并没有实现内部序列化操作
                // get->btye[]->反序列化->Object(Seckill)
                //采用自定义序列化---引入protostuff的相关依赖
                //protostuff : pojo
                byte[] bytes = jedis.get(key.getBytes());
                //缓存重新获取到
                if(bytes != null){
                    //空对象
                    Seckill seckill = schema.newMessage();
                    ProtostuffIOUtil.mergeFrom(bytes,seckill,schema);
                    //seckill被反序列
                    return seckill;
                }
            }finally {
                jedis.close();
            }

        } catch (Exception e){
            logger.error(e.getMessage(),e);
        }
        return null;
    }

    public String putSeckill(Seckill seckill){
        // set Object(Seckill) -> 序列化 -> bytes[] -> 发送给redis
        try{
            Jedis jedis = jedisPool.getResource();
            try{
                String key = "seckill:"+seckill.getSeckillId();
                byte[] bytes = ProtostuffIOUtil.toByteArray(seckill,schema,
                        LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE));
                //超时缓存
                int timeout = 60*60;//1小时
                String result = jedis.setex(key.getBytes(), timeout, bytes);
                return result;
            }finally {
                jedis.close();
            }
        } catch (Exception e){
            logger.error(e.getMessage(),e);
        }
        return null;
    }
}

新增单元测试,因为RedisDao不属于MyBatis的范畴,所以我们需要手动在spring-dao.xml中注入:

<!--RedisDao-->
<bean id="redisDao" class="org.luyangsiyi.seckill.dao.cache.RedisDao">
    <constructor-arg index="0" value="localhost"/>
    <constructor-arg index="1" value="6379"/>
</bean>

单元测试:

package org.luyangsiyi.seckill.dao.cache;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.luyangsiyi.seckill.dao.SeckillDao;
import org.luyangsiyi.seckill.entity.Seckill;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
 * Created by luyangsiyi on 2020/2/24
 */
@RunWith(SpringJUnit4ClassRunner.class)
//告诉junit spring配置文件
@ContextConfiguration({"classpath:spring/spring-dao.xml"})
public class RedisDaoTest {

    long id = 1001;

    @Autowired
    private RedisDao redisDao;

    @Autowired
    private SeckillDao seckillDao;

    @Test
    public void testSeckill() {
        //get and put
        Seckill seckill = redisDao.getSeckill(id);
        if(seckill == null){
            seckill = seckillDao.queryById(id);
            if(seckill != null){
                String result = redisDao.putSeckill(seckill);
                System.out.println(result);
                seckill = redisDao.getSeckill(id);
                System.out.println(seckill);
            }
        }
    }
}

修改exportSeckillUrl()函数:

@Override
public Exposer exportSeckillUrl(long seckillId) {
    // 优化点:缓存优化:超时的基础上维护一致性
    //1:访问redis
    Seckill seckill = redisDao.getSeckill(seckillId);
    if(seckill == null){
        //2:访问数据库
        seckill = seckillDao.queryById(seckillId);
        if(seckill == null){
            return new Exposer(false,seckillId);
        } else {
            //3:放入redis
            redisDao.putSeckill(seckill);
        }
    }

    Date startTime = seckill.getStartTime();
    Date endTime = seckill.getEndTime();
    Date nowTime = new Date();
    if(nowTime.getTime() < startTime.getTime()
        || nowTime.getTime() > endTime.getTime()){
        return new Exposer(false, seckillId, nowTime.getTime(),
                startTime.getTime(),endTime.getTime());
    }
    //转化特定字符串的过程,不可逆
    String md5 = getMD5(seckillId);
    return new Exposer(true,md5,seckillId);
}

然后继续测试SeckillServiceTest()函数,可以在终端查询到缓存的结果:
在这里插入图片描述

三、并发优化

1、简单优化:insert购买明细—update减库存rowLock—commit/rollback释放锁,因为insert命令使用的两个主键可能产生冲突的情况比较少,所以我们可以不对其加锁,以降低MySQL锁的时间。
executeSeckill()函数优化,修改处理逻辑:

public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException, RepeatKillException, SeckillClosedException {
    if(md5 == null || !md5.equals(getMD5(seckillId))){
        throw new SeckillException("seckill data rewrite");
    }
    //执行秒杀逻辑:减库存+记录购买行为
    //优化:修改逻辑为记录购买行为+减库存---将网络延迟带来的影响降低一半
    Date nowTime = new Date();
    try{
        //减库存成功后,记录购买行为
        int insertCount = successKilledDao.insertSuccessKilled(seckillId,userPhone);
        //唯一
        if(insertCount<=0){
            //重复秒杀
            throw new RepeatKillException("seckill repeated");
        } else{
            //减库存,热点商品竞争
            int updateCount = seckillDao.reduceNumber(seckillId,nowTime);
            if(updateCount <= 0){
                //没有更新到记录,秒杀结束
                throw new SeckillClosedException("seckill is closed");
            } else {
                //秒杀成功
                SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId,userPhone);
                return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS,successKilled);
            }
        }
    } catch(SeckillClosedException e1) {
        throw e1;
    } catch(RepeatKillException e2) {
        throw e2;
    } catch (Exception e){
        logger.error(e.getMessage(),e);
        //所有编译器异常,转化为运行期异常
        throw new SeckillException("seckill inner error"+e.getMessage());
    }

2、深度优化:事务SQL在MySQl端执行(存储过程)
存储过程:
1.存储过程优化:事务行级锁持有的时间
2.不要过度依赖存储过程
3.简单的逻辑,可以应用存储过程
4.QPS:一个秒杀单6000/qps

新建seckill.sql,编写存储过程的实现:

-- 秒杀执行的存储过程
delimiter $$ -- console 中的 ; 转化为 $$
-- 定义存储过程
-- 参数:in输入参数;out输出参数
-- row_count():返回上一条修改类型sql(delete,insert,update)的影响行数
-- row_count():0 未修改数据;>0 修改的行数;<0 sql错误/未执行修改sql。
create  procedure `seckill`.`execute_seckill`
    (in v_seckill_id bigint, in v_phone bigint,
    in v_kill_time timestamp, out r_result int)
    begin
        declare insert_count int default 0;
        start transaction;
        insert ignore into success_killed
            (seckill_id,user_phone,create_time)
            values (v_seckill_id,v_phone,v_kill_time);
        select row_count() into insert_count;
        if (insert_count = 0) then
            rollback ;
            set r_result = -1;
        elseif (insert_count < 0) then
            rollback;
            set r_result = -2;
        else
            update seckill
                set number = number - 1
            where seckill_id = v_seckill_id
                and end_time > v_kill_time
                and start_time < v_kill_time
            and number > 0;
            select row_count() into insert_count;
            if(insert_count = 0) then
                rollback ;
                set r_result = 0;
            elseif(insert_count < 0) then
                rollback ;
                set r_result = -2;
            else
                commit;
                set r_result = 1;
            end if ;
        end if ;
     end;
$$
-- 存储过程定义结束

delimiter ;

set @r_result = -3;
-- 执行存储过程
call execute_seckill(1000,13314455555,now(),@r_result);
-- 获取结果
select @r_result;

-- 存储过程
-- 1.存储过程优化:事务行级锁持有的时间
-- 2.不要过度依赖存储过程
-- 3.简单的逻辑,可以应用存储过程
-- 4.QPS:一个秒杀单6000/qps

然后我们新增一个调用存储过程来实现秒杀的接口函数:

/**
 * 执行秒杀操作 by 存储过程
 * @param seckillId
 * @param userPhone
 * @param md5
 * @return
 * @throws SeckillException
 * @throws RepeatKillException
 * @throws SeckillClosedException
 */
SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5);

在dao中添加对应方法:

/**
 * 使用存储过程执行秒杀
 * @param paramMap
 */
void killByProcedure(Map<String,Object> paramMap);

编写其实现Mapper映射:

<!--mybatis调用存储过程-->
<select id="killByProcedure" statementType="CALLABLE">
    call execute_seckill(
        #{seckillId,jdbcType=BIGINT,mode=IN},
        #{phone,jdbcType=BIGINT,mode=IN},
        #{killTime,jdbcType=TIMESTAMP,mode=IN},
        #{result,jdbcType=INTEGER,mode=OUT}
        )
</select>

然后编写Service的实现:

@Override
public SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5) {
    if(md5 == null || !md5.equals(getMD5(seckillId))){
        return new SeckillExecution(seckillId,SeckillStatEnum.DATA_REWRITE);
    }
    Date killTime = new Date();
    Map<String,Object> map = new HashMap<>();
    map.put("seckillId",seckillId);
    map.put("phone",userPhone);
    map.put("killTime",killTime);
    map.put("result",null);
    //执行存储过,result被赋值
    try{
        seckillDao.killByProcedure(map);
        //获取result,MapUtils需要在pom.xml中引入commons-collections依赖
        int result = MapUtils.getInteger(map,"result",-2);
        if(result == 1){
            SuccessKilled sk = successKilledDao.queryByIdWithSeckill(seckillId,userPhone);
            return new SeckillExecution(seckillId,SeckillStatEnum.SUCCESS,sk);
        } else {
            return new SeckillExecution(seckillId,SeckillStatEnum.stateOf(result));
        }
    } catch (Exception e){
        logger.error(e.getMessage(),e);
        return new SeckillExecution(seckillId,SeckillStatEnum.INNER_ERROR);
    }
}

注意使用MapUtils时需要引入依赖:

<dependency>
  <groupId>commons-collections</groupId>
  <artifactId>commons-collections</artifactId>
  <version>3.2.2</version>
</dependency>

编写测试:

@Test
public void executeSeckillProcedure(){
    long seckillId = 1000;
    long phone = 13315566780L;
    Exposer exposer = seckillService.exportSeckillUrl(seckillId);
    if(exposer.isExposed()){
        String md5 = exposer.getMd5();
        SeckillExecution seckillExecution = seckillService.executeSeckillProcedure(seckillId, phone, md5);
        logger.info(seckillExecution.getStateInfo());
    }
}

此时Controller中修改实现的函数为executeSeckillProcedure(),可以在浏览器中进行测试。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 我们可以使用Java语言和SSM框架来实现图书管理系统。我们可以使用Spring框架中的IOC容器来解耦各组件,从而让系统的代码结构更加清晰,同时也可以使用Spring框架中的AOP技术来实现系统的权限控制和日志记录。此外,我们还可以使用Mybatis框架来实现数据库的操作,从而更加方便地实现图书管理系统的功能。 ### 回答2: 基于SSM(Spring+SpringMVC+MyBatis)框架实现图书管理系统需要以下步骤: 1. 环境搭建:安装Java开发环境、Maven项目管理工具、Tomcat服务器,引入SSM相关的依赖包。 2. 创建数据库:使用MySQL等数据库管理系统创建图书管理系统所需的数据库,包括图书、借阅记录等表。 3. 创建实体类:根据数据库表设计实体类,包括图书、借阅记录等。 4. 配置数据源:在Spring配置文件中配置数据源,连接数据库。 5. 创建Dao层:使用MyBatis的注解或XML配置方式实现图书相关的数据库操作,包括图书的增删改查。 6. 创建Service层:编写图书相关的业务逻辑代码,封装对Dao层的调用。 7. 创建Controller层:编写图书相关的请求处理代码,接收请求参数并调用相应的Service方法。 8. 创建视图层:使用JSP、HTML等技术实现前端页面,展示图书列表、借阅记录等信息。 9. 配置SpringMVC:在Spring配置文件中配置SpringMVC相关的视图解析器、控制器等。 10. 部署运行:将项目打包成war文件,部署到Tomcat服务器中运行。 通过以上步骤,我们就可以基于SSM框架成功实现图书管理系统。用户可以在系统中进行图书查询、借阅、归还等操作,管理员可以进行图书的管理和操作日志的查看。 ### 回答3: 基于SSM实现图书管理系统,可以分为以下几个步骤: 1. 设计数据库结构: 首先,需要设计数据库表的结构。可以创建几个表,如图书表、读者表、借阅记录表等。每个表都有相应的字段来存储不同的信息。 2. 搭建项目环境: 在Java开发工具中,搭建基于SSM框架的项目环境。SSM框架由Spring、SpringMVC和MyBatis组成,分别负责实现控制反转、Web层和数据持久层的功能。 3. 编写实体类: 根据数据库表的结构,编写对应的实体类。每个实体类对应数据库表的字段,并且有相应的成员变量和访问方法。 4. 配置MyBatis: 在项目的配置文件中,配置MyBatis的相关信息,如数据库连接、映射文件路径等。映射文件中定义了SQL语句与实体类之间的映射关系。 5. 编写数据访问层: 利用MyBatis框架,编写数据访问层的代码。主要负责与数据库进行交互,执行SQL语句,并将查询结果封装到实体类中。 6. 编写业务逻辑层: 在Spring框架的帮助下,编写业务逻辑层的代码。该层处理具体的业务逻辑,如图书的增删改查、读者的注册登录等功能。 7. 编写控制器: 利用SpringMVC框架,编写控制器的代码。该层负责接收前端请求,调用相应的业务逻辑处理,并返回结果给前端。 8. 编写前端页面: 使用HTML、CSS和JavaScript等技术,编写前端页面。可以根据需求设计图书借阅、读者管理等功能的界面,并与后端进行交互。 9. 测试和调试: 对整个系统进行测试和调试,验证功能的正确性和性能的优化。 10. 部署和上线: 将完成的系统部署到服务器上,并上线供用户使用。 以上就是基于SSM框架实现图书管理系统的大致步骤,通过这个系统,可以实现图书的增删改查、借阅归还等功能,方便用户管理图书馆的资源。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值