秒杀系统优化方案(下)吐血整理 - 开拖拉机的蜡笔小新 - 博客园

mikel阅读(534)

来源: 秒杀系统优化方案(下)吐血整理 – 开拖拉机的蜡笔小新 – 博客园

3. 深入优化设计

3.1   初始方案问题分析

在前面针对数据库的优化中,由于数据库行级锁存在竞争造成大量的串行阻塞,我们使用了存储过程(或者触发器)等技术绑定操作,整个事务在MySQL端完成,把整个热点执行放在一个过程当中一次性完成,可以屏蔽掉网络延迟时间,减少行级锁持有时间,提高事务并发访问速度。

可是问题时并发的流量实际上都是直接穿透让MYSQL自己去抗,比如说库存是否卖完以及用户是否重复秒杀都完全是靠查询数据库去判断,造成数据库不必要的负担非常大,然而这些都可以放在缓存做一个标记在服务层进行拦截,对于中小规模的并发还可以,但是真正的超高并发,显然这个还不完善。

3.2    优化的方向和思路

方向:将请求尽量拦截在系统上游

传统秒杀系统之所以挂,请求都压倒了后端数据层,数据读写锁冲突严重,并发高响应慢,几乎所有请求都超时,流量虽大,下单成功的有效流量甚小【一趟火车其实只有2000张票,200w个人来买,基本没有人能买成功,请求有效率为0】

思路:限流和削峰

限流:屏蔽掉无用的流量,允许少部分流量流向后端。

削峰:瞬时大流量峰值容易压垮系统,解决这个问题是重中之重。常用的消峰方法有异步处理、缓存和消息中间件等技术。

 

异步处理:秒杀系统是一个高并发系统,采用异步处理模式可以极大地提高系统并发量,其实异步处理就是削峰的一种实现方式。

缓存:秒杀系统本身是一个典型的读多写少的应用场景【一趟火车其实只有2000张票,200w个人来买,最多2000个人下单成功,其他人都是查询库存,写比例只有0.1%,读比例占99.9%】,非常适合使用缓存。

消息队列:消息队列可以削峰,将拦截大量并发请求,这也是一个异步处理过程,后台业务根据自己的处理能力,从消息队列中主动的拉取请求消息进行业务处理。

3.3   前端优化

3.3.1   静态资源缓存

1. 页面静态化

对商品详情和订单详情进行页面静态化处理,页面是存在html,动态数据是通过接口从服务端获取,实现前后端分离,静态页面无需连接数据库打开速度较动态页面会有明显提高。

2.页面缓存

通过CDN缓存静态资源,来抗峰值。不使用CDN的话也可以通过在手动渲染得到的html页面缓存到redis。

3.3.2   限流手段

1. 使用数学公式验证码

描述:点击秒杀前,先让用户输入数学公式验证码,验证正确才能进行秒杀。

好处:

1)防止恶意的机器人和爬虫

2)分散用户的请求

实现:

1)前端通过把商品id作为参数调用服务端创建验证码接口

2)服务端根据前端传过来的商品id和用户id生成验证码,并将商品id+用户id作为key,生成的验证码作为value存入redis,同时将生成的验证码输入图片写入imageIO让前端展示。

3)将用户输入的验证码与根据商品id+用户id从redis查询到的验证码对比,相同就返回验证成功,进入秒杀;不同或从redis查询的验证码为空都返回验证失败,刷新验证码重试

 

2. 禁止重复提交

用户提交之后按钮置灰,禁止重复提交

3.4    中间代理层

可利用负载均衡(例如反响代理Nginx等)使用多个服务器并发处理请求,减小服务器压力。

3.5     后端优化

3.5.1   控制层(网关层)

限制同一UserID访问频率:尽量拦截浏览器请求,但针对某些恶意攻击或其它插件,在服务端控制层需要针对同一个访问uid,限制访问频率。

1.    利用缓存

设置缓存有效时间,在缓存中计数,如果在缓存的有效时间内请求的次数超了的话,就返回请求访问太频繁。

2.    利用RateLimiter

RateLimiter是guava提供的基于令牌桶算法的限流实现类,通过调整生成token的速率来限制用户频繁访问秒杀页面,从而达到防止超大流量冲垮系统。(令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。

3.5.2   服务层

当用户量非常大的时候,拦截流量后的请求访问量还是非常大,此时仍需进一步优化。

1.    业务分离:将秒杀业务系统和其他业务分离,单独放在高配服务器上,可以集中资源对访问请求抗压。——应用的拆分

2.    采用消息队列缓存请求:将大流量请求写到消息队列缓存,利用服务器根据自己的处理能力主动到消息缓存队列中抓取任务处理请求,数据库层订阅消息减库存,减库存成功的请求返回秒杀成功,失败的返回秒杀结束。

3.    利用缓存应对读请求:对于读多写少业务,大部分请求是查询请求,所以可以读写分离,利用缓存分担数据库压力。

4.    利用缓存应对写请求:缓存也是可以应对写请求的,可把数据库中的库存数据转移到Redis缓存中,所有减库存操作都在Redis中进行,然后再通过后台进程把Redis中的用户秒杀请求同步到数据库中。

可以将缓存和消息中间件 组合起来,缓存系统负责接收记录用户请求,消息中间件负责将缓存中的请求同步到数据库。

 

方案:本地标记 + redis预处理 + RabbitMQ异步下单 + 客户端轮询

描述:通过三级缓冲保护,1、本地标记 2、redis预处理 3、RabbitMQ异步下单,最后才会访问数据库,这样做是为了最大力度减少对数据库的访问。

实现:

  1. 在秒杀阶段使用本地标记对用户秒杀过的商品做标记,若被标记过直接返回重复秒杀,未被标记才查询redis,通过本地标记来减少对redis的访问
  2. 抢购开始前,将商品和库存数据同步到redis中,所有的抢购操作都在redis中进行处理,通过Redis预减少库存减少数据库访问
  3. 为了保护系统不受高流量的冲击而导致系统崩溃的问题,使用RabbitMQ用异步队列处理下单,实际做了一层缓冲保护,做了一个窗口模型,窗口模型会实时的刷新用户秒杀的状态。
  4. client端用js轮询一个接口,用来获取处理状态

3.5.3  数据库层

数据库层是最脆弱的一层,一般在应用设计时在上游就需要把请求拦截掉,数据库层只承担“能力范围内”的访问请求。所以,上面通过在服务层引入队列和缓存,让最底层的数据库高枕无忧。但依然可以进行如下方向的优化:

对于秒杀系统,直接访问数据库的话,存在一个【事务竞争优化】问题,可使用存储过程(或者触发器)等技术绑定操作,整个事务在MySQL端完成,把整个热点执行放在一个过程当中一次性完成,可以屏蔽掉网络延迟时间,减少行级锁持有时间,提高事务并发访问速度。

 

3.7  优化秒杀流程

  1. 秒杀活动开始之前有个活动倒计时,时间到了则会放开秒杀的权限,并生成一个验证码展示在前面页面,并把验证结果存在redis中,这里利用redis有过期时间的特性,也给验证码的缓存加了个过期时间。这里的redis缓存用的是redis的string类型。
  2. 在秒杀之前先要填一个验证码verifyCode,点击秒杀按钮时,先发送ajax请求到后台获取真实的秒杀地址path,这里秒杀地址是隐藏的,目的是防止有人恶意刷秒杀接口。所谓隐藏地址,其实是在请求地址中加一段随机字符串,这段字符串是变化的,因此秒杀请求地址是动态的;
  3. 先说下如何获取真实的秒杀地址,后台先访问redis,验证一下这个验证码有没有过期以及这个verifyCode是不是正确,验证码验证通过后,先删除这个验证码缓存,然后生成真实地址;
  4. 真实地址随机字符串由uuid以及md5加密生成,并且保存在redis中,并且设置了有效期;
  5. 从浏览器端向秒杀地址发起请求,带上path参数去后台调用真正的秒杀接口,下面是秒杀接口的逻辑
  6. 访问redis,验证path有没有过期,以及是不是正确。这里验证path以及上面的校验验证码,都是用userId对应生成的一个key值去取redis中的数据;
  7. path验证通过后,先访问内存标识,看秒杀的这个商品有没有卖完,减少对redis的不必要访问。每一种参与秒杀活动的商品都在内存里用HashMap设置了一个标识,标识某个商品id商品是否卖完了。这里的是否卖完的内存标识设置以及每种参与秒杀商品的库存存入redis是在系统启动时做的;
  8. 如果内存标识中这个商品没有卖完,则要看这个用户在这次活动中是否重复秒杀,因为我们的秒杀规则是一个用户id对于某个商品id的商品只能秒杀一件。如何判断该用户有没有秒杀过这件商品呢,秒杀记录也保存在redis缓存中
  9. 如果判断秒杀过则返回提示,如果没有秒杀过,继续;
  10. 上面说过系统加载时redis中保存了各商品对应的库存,这里用到redis的原子操作的方法decr,将对应商品的库存减1,此时数据库时的库存还没有减,因此是预减库存
  11. desc方法返回该商品此时的库存,如果小于0,说明商品已经卖完了,此次秒杀无效,并且设置该商品的内存标识为true,表示已卖完
  12. 正确地预减库存后,然后就要真正操作数据库了,数据库一般是性能瓶颈,比较耗时,因此决定用异步方式处理。对于每一条秒杀请求存入消息队列RabbitMQ中,消息体中要包含哪个用户秒杀哪个商品的信息,这里是封装了一个消息体类,这样一个秒杀请求就进入了消息队列,一个秒杀请求还没有完成,真正的秒杀请求的完成得要持久化到数据库,生成订单,减了数据库的库存才能算数,这时在客户端显示的一般是排队中,比如以前在抢购小米手机时,我就看到这样的展示,过一会再刷新页面就显示没抢到;
  13. 消息队列处理秒杀请求。先从消息体中解析出用户id和商品id,查数据库看这个商品是否卖完了查数据库看该用户对于这个商品是否有过秒杀记录数据库减库存,数据库生成订单,这两项持久化地写数据库操作放在同一个事务中,要么都执行成功,要么都失败。并把秒杀记录对象,包括秒杀单号、订单号、用户id、商品id,存入redis如果数据库减库存失败,表明商品卖完了,则要在redis中设置该商品已卖完的标识消息队列处理秒杀请求。先从消息体中解析出用户id和商品id,查数据库看这个商品是否卖完了查数据库看该用户对于这个商品是否有过秒杀记录
  14. 数据库减库存,数据库生成订单,这两项持久化地写数据库操作放在同一个事务中,要么都执行成功,要么都失败。并把秒杀记录对象,包括秒杀单号、订单号、用户id、商品id,存入redis如果数据库减库存失败,表明商品卖完了,则要在redis中设置该商品已卖完的标识
  15. ajax发起秒杀请求,秒杀请求的处理逻辑最后也只是把这条请求放入消息队列,并不能返回是否秒杀成功的结果。因此,当秒杀请求正确响应后,即请求放入消息队列后,需要另外一个请求去轮询秒杀结果,秒杀成功的标志是生成秒杀订单,并把秒杀订单对象放入redis中。所以轮询秒杀结果,只用去轮询redis中是否有对应于该用户的该商品的秒杀订单对象,如果有,则表明秒杀成功,并在前台给出提示。

上面的秒杀流程对应的流程图如下:
步骤1到12,主体是redis预减库存,生成消息队列:

 

步骤13到14是处理消息队列:

步骤15,是客户端请求秒杀结果:

 

4. 问题解析

1.      如何解决库存的超卖问题?

卖超原因:

(1)一个用户同时发出了多个请求,如果库存足够,没加限制,用户就可以下多个订单。(2)减库存的sql上没有加库存数量的判断,并发的时候也会导致把库存减成负数。

解决办法:

(1):在后端的秒杀表中,对user_id和goods_id加唯一索引,确保一个用户对一个商品绝对不会生成两个订单。

(2):我们的减库存的sql上应该加上库存数量的判断

数据库自身是有行级锁的,每次减库存的时候判断count>0,它实际上是串行的执行update的,因此绝对不会卖超!。

UPDATE seckill

        SET number = number-1

        WHERE seckill_id=#{seckillId}

        AND start_time <#{killTime}

        AND end_time >= #{killTime}

        AND number > 0;

2.    如何解决少卖问题—Redis预减成功而DB扣库存失败?

前面的方案中会出现一个少卖的问题。Redis在预减库存的时候,在初始化的时候就放置库存的大小,redis的原子减操作保证了多少库存就会减多少,也就会在消息队列中放多少。

现在考虑两种情况:

1)数据库那边出现非库存原因比如网络等造成减库存失败,而这时redis已经减了。

2)万一一个用户发出多个请求,而且这些请求恰巧比别的请求更早到达服务器,如果库存足够,redis就会减多次,redis提前进入卖空状态,并拒绝。不过这两种情况出现的概率都是非常低的。

两种情况都会出现少卖的问题,实际上也是缓存和数据库出现不一致的问题

但是我们不是非得解决不一致的问题,本身使用缓存就难以保证强一致性:

在redis中设置库存比真实库存多一些就行。

3.   秒杀过程中怎么保证redis缓存和数据库的一致性?

在其他一般读大于写的场景,一般处理的原则是:缓存只做失效,不做更新。

采用Cache-Aside pattern:

失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。

更新:先把数据存到数据库中,成功后,再让缓存失效。

4.  Redis中的库存如何与DB中的库存保持一致?

Redis中的数量不是库存,它的作用仅仅时候只是为了阻挡多余的请求透传到db,起到一个保护DB的作用。因为秒杀商品的数量是有限的,比如只有10个,让1万个请求去访问DB是没有意义的,因为最多只有10个请求会下单成功,剩余的9990个请求都是无效的,是可以不用去访问db而直接失败的。

因此,这是一个伪问题,我们是不需要保持一致的。

5.   为什么要隐藏秒杀接口?

html是可以被右键->查看源代码,如果秒杀地址写死在源文件中,是很容易就被恶意用户拿到的,就可以被机器人利用来刷接口,这对于其他用户来说是不公平的,我们也不希望看到这种情况。所以我们可以控制让用户在没有到秒杀时间的时候不能获取到秒杀地址,只返回秒杀的开始时间。

当到秒杀时间的时候才返回秒杀地址即seckill_id以及根据seckill_id和salt加密的MD5,前端再次拿着seckill_id和MD5才能执行秒杀。假如用户在秒杀开始前猜测到秒杀地址seckill_id去请求秒杀,也是不会成功的,因为它拿不到需要验证的MD5。这里的MD5相当于是用户进行秒杀的凭证。

6.   一个秒杀系统,500用户同时登陆访问服务器A,服务器B如何快速利用登录名(假设是电话号码或者邮箱)做其他查询?

主从复制,读写分离

秒杀系统优化方案(上)吐血整理 - 开拖拉机的蜡笔小新 - 博客园

mikel阅读(595)

来源: 秒杀系统优化方案(上)吐血整理 – 开拖拉机的蜡笔小新 – 博客园

前一段时间好好研究了秒杀的问题,我把里面的问题好好总结了,可以说是比较全面的了,真的是吐血整理了。

由于我先是在word中整理的,格式都整理得比较好,放到博客上格式挺难调,暂时按word的格式来吧,有时间了在好好排版下。

主要需要解决的问题有两个:

  1. 高并发对数据库产生的压力
  2. 竞争状态下如何解决库存的正确减少(超卖问题)

优化的思路:

1) 尽量将请求拦截在系统上游

2)读多写少经量多使用缓存
3) redis缓存 +RabbitMQ+ mySQL 批量入库

1.   初始秒杀设计

1.1 业务分析

秒杀系统业务流程如下:

由图可以发现,整个系统其实是针对库存做的系统。用户成功秒杀商品,对于我们系统的操作就是:1.减库存。2.记录用户的购买明细。下面看看我们用户对库存的业务分析:

记录用户的秒杀成功信息,我们需要记录:1.谁购买成功了。2.购买成功的时间/有效期。这些数据组成了用户的秒杀成功信息,也就是用户的购买行为。

为什么我们的系统需要事务?

1.若是用户成功秒杀商品我们记录了其购买明细却没有减库存。导致商品的超卖

2.减了库存却没有记录用户的购买明细。导致商品的少卖。对于上述两个故障,若是没有事务的支持,损失最大的无疑是我们的用户和商家。在MySQL中,它内置的事务机制,可以准确的帮我们完成减库存和记录用户购买明细的过程。

1.2  难点分析

当用户A秒杀id为10的商品时,此时MySQL需要进行的操作是:

1.开启事务。2.更新商品的库存信息。3.添加用户的购买明细,包括用户秒杀的商品id以及唯一标识用户身份的信息如电话号码等。4.提交事务。

若此时有另一个用户B也在秒杀这件id为10的商品,他就需要等待,等待到用户A成功秒杀到这件商品,然后MySQL成功的提交了事务他才能拿到这个id为10的商品的锁从而进行秒杀,而同一时间是不可能只有用户B在等待,肯定是有很多很多的用户都在等待竞争行级锁。秒杀的难点就在这里,如何高效的处理这些竞争?如何高效的完成事务?

1.3 功能实现

我们只是实现秒杀的一些功能:1.秒杀接口的暴露。2.执行秒杀的操作。3.相关查询,比如说列表查询,详情页查询。我们实现这三个功能即可。

1.4 数据库设计

Seckill秒杀表单

Success_seckill购买明细表

在购买明细表中seckill_id和user_phone是联合主键,当重复秒杀的时候,加入ignore防止报错,只是会返回0,表示重复秒杀。

INSERT ignore INTO success_killed(seckill_id,user_phone,state)
VALUES (#{seckillId},#{userPhone},0)

在购买明细表中seckill_id和user_phone是联合主键,当重复秒杀的时候,加入ignore防止报错,只是会返回0,表示重复秒杀。

INSERT ignore INTO success_killed(seckill_id,user_phone,state)
VALUES (#{seckillId},#{userPhone},0)

1.5 DAO层设计

秒杀表的DAO:减库存(id,nowtime)、由id查询商品、由偏移量查询商品

购买明细表的DAO:插入购买明细、根据商品id查询明细SucceesKill对象(携带Seckill对象)—mybatis的复合查询

减库存和增加明细的sql

复制代码
<update id="reduceNumber">
        UPDATE seckill
        SET number = number-1
        WHERE seckill_id=#{seckillId}
        AND start_time <![CDATA[ <= ]]> #{killTime}
        AND end_time >= #{killTime}
        AND number > 0;
</update>
<insert id="insertSuccessKilled">
        <!--当出现主键冲突时(即重复秒杀时),会报错;不想让程序报错,加入ignore-->
        INSERT ignore INTO success_killed(seckill_id,user_phone,state)
        VALUES (#{seckillId},#{userPhone},0)
 </insert>
复制代码

 

1.6 Service层设计

暴露秒杀地址(接口)DTO

复制代码
public class Exposer {
    //是否开启秒杀
    private boolean exposed;
    //加密措施
    private String md5;
    private long seckillId;
    //系统当前时间(毫秒)
    private long now;
    //秒杀的开启时间
    private long start;
    //秒杀的结束时间
    private long end;}
复制代码

 

封装执行秒杀后的结果:是否秒杀成功

复制代码
public class SeckillExecution {
    private long seckillId;
    //秒杀执行结果的状态
    private int state;
    //状态的明文标识
    private String stateInfo;
    //当秒杀成功时,需要传递秒杀成功的对象回去
    private SuccessKilled successKilled;}
复制代码

 

秒杀过程

接口暴露:

复制代码
public Exposer exportSeckillUrl(long seckillId) {
        //缓存优化
        Seckill seckill = getById(seckillId);
        //若是秒杀未开启
        Date startTime = seckill.getStartTime();
        Date endTime = seckill.getEndTime();
        //系统当前时间
        Date nowTime = new Date();
        if (startTime.getTime() > nowTime.getTime() || endTime.getTime() < nowTime.getTime()) {
            return new Exposer(false, seckillId, nowTime.getTime(), startTime.getTime(), endTime.getTime());
        }
        //秒杀开启,返回秒杀商品的id、用给接口加密的md5
        String md5 = getMD5(seckillId);
        return new Exposer(true, md5, seckillId);
}
复制代码

如果当前时间还没有到秒杀时间或者已经超过秒杀时间,秒杀处于关闭状态,那么返回秒杀的开始时间和结束时间;如果当前时间处在秒杀时间内,返回暴露地址(秒杀商品的id、用给接口加密的md5)

为什么要进行MD5加密?

我们用MD5加密的方式对秒杀地址(seckill_id)进行加密,暴露给前端用户。当用户执行秒杀的时候传递seckill_id和MD5,程序拿着seckill_id根据设置的盐值计算MD5,如果与传递的md5不一致,则表示地址被篡改了。

 

为什么要进行秒杀接口暴露的控制或者说进行秒杀接口的隐藏?

现实中有的用户回通过浏览器插件提前知道秒杀接口,填入参数和地址来实现自动秒杀,这对于其他用户来说是不公平的,我们也不希望看到这种情况。所以我们可以控制让用户在没有到秒杀时间的时候不能获取到秒杀地址,只返回秒杀的开始时间。当到秒杀时间的时候才

返回秒杀地址即seckill_id以及根据seckill_id和salt加密的MD5,前端再次拿着seckill_id和MD5才能执行秒杀。假如用户在秒杀开始前猜测到秒杀地址seckill_id去请求秒杀,也是不会成功的,因为它拿不到需要验证的MD5。这里的MD5相当于是用户进行秒杀的凭证。

 

执行秒杀:

复制代码
 //秒杀是否成功,成功: 增加明细,减库存;失败:抛出异常,事务回滚
    @Transactional
    public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)  throws SeckillException, RepeatKillException, SeckillCloseException {
        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) {
                    //没有更新库存记录,说明秒杀结束或者是已经卖完 rollback
                    throw new SeckillCloseException("seckill is closed");
                } else {
                    //秒杀成功,得到成功插入的明细记录,并返回成功秒杀的信息 commit
                    SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
                    return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled);
                }
            }
        } catch (SeckillCloseException e1) {
            throw e1;
        } catch (RepeatKillException e2) {
            throw e2;
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            //所以编译期异常转化为运行期异常
            throw new SeckillException("seckill inner error :" + e.getMessage());
        }
    }
复制代码

首先检查用户是否已经登录,查看cookie中是否有phone的信息,如果没有,返回没有注册的错误信息。

接着执行秒杀,首先验证md5,看地址是否被篡改。先增加明细(为什么要先增加明细见后面优化的过程),看是否该明细被重复插入,即用户是否重复秒杀,如果是,抛异常。然后减库存,因为sql在减库存的时候判断了当前时间和秒杀时间是否对应,如果数据库update返回0没有更新库存记录,说明秒杀结束;或者是库存已经没有主动抛出错误rollback。(前面在获取秒杀地址的时候已经挡住了秒杀关闭的请求(没到时间或者时间已过),然后从获取到秒杀地址到执行秒杀还可能会在这段时间秒杀结束)

最后秒杀成功,得到购买明细信息,接着commit。

注意事务在这里的处理:

Spring事务异常回滚,捕获异常不抛出就不会回滚

1.7 Web层设计

交互流程

2.   初始优化设计

红色部分代表可能高并发的点,绿色表示没有影响

2.1 详情页缓存

通过CDN缓存静态资源,来抗峰值。

动静态数据分离

详情页静态资源是部署在CDN节点中,也就是说访问静态资源或者详情页是不用访问我们的系统的。

限流小技巧:用户提交之后按钮置灰,禁止重复提交 

为什么要单独ajax请求获取服务器的时间?

为了保持时间一致,因为详情页放在CDN上和系统存放的位置是分离的。

2.2 秒杀接口地址缓存

无法使用CDN是因为,CDN适合的请求的资源是不易变化的。

秒杀接口是变化的,可以使用redis服务端缓存可以用集群抗住非常大的并发。1秒钟可以承受10万qps。多个Redis组成集群,可以到100w个qps

一致性:当秒杀的对象改变的时候修改我们的数据库同时修改缓存。

原本查询秒杀商品时是通过主键直接去数据库查询的,选择将数据缓存在Redis,在查询秒杀商品时先去Redis缓存中查询,以此降低数据库的压力。如果在缓存中查询不到数据再去数据库中查询,再将查询到的数据放入Redis缓存中,这样下次就可以直接去缓存中直接查询到。

这里有一个继续优化的点:在redis中存放对象是将对象序列化成byte字节。

通过Jedis储存对象的方式有大概三种

  1. 本项目采用的方式:将对象序列化成byte字节,最终存byte字节;
  2. 对象转hashmap,也就是你想表达的hash的形式,最终存map;
  3. 对象转json,最终存json,其实也就是字符串

其实如果你是平常的项目,并发不高,三个选择都可以,这种情况下以hash的形式更加灵活,可以对象的单个属性,但是问题来了,在秒杀的场景下,三者的效率差别很大。

10w数据

时间

内存占用

存json

10s

14M

存byte

6s

6M

存jsonMap

10s

20M

存byteMap

4s

4M

取json

7s

取byte

4s

取jsonmap

7s

取bytemap

4s

bytemap最快啊,为啥不用啊,因为项目用了超级高性能的自定义序列化工具protostuff。

2.3 秒杀操作优化

Mysql真的低效吗?

在mysql端一条update压力测试约4wQPS,即使是现在最好的秒杀产品应该也达不到这个数字。

然而实际上远没有这么高的QPS,那么时间消耗在哪呢?

串行化操作,大量的堵塞

2.3.1 瓶颈分析

客户端执行update,当我们的sql通过网络发送到mysql的时候,这本身就有网络延迟在里面,并且还有GC的时间,GC又分为新生代GC和老年代GC,新生代会暂停所有的事务代码,也就是我们的java代码,一般在几十毫秒

也即是说如果由java客户端去控制这些事务的话,update减库存,网络延迟,update数据操作结果返回,然后执行GC;然后执行insert,发生网络延迟,等待insert执行结果返回,也可能出现GC,最后commit或者rollback。当这些执行完了之后,第二个等待行锁的线程才有可能拿到这个数据行的锁,再去执行update减库存。

不是我们的mysql慢,也不是java慢,可能存在我们的java客户端执行这些sql,然后等待这些sql的结果,再去做判断再去执行这些sql,这一长串的事务在java客户端执行,但是java客户端和数据库之间会有网络延迟,或者是GC这些时间也要加载事务的执行周期里面,而同一行的事务是串行化的。

那么我们的QPS分析就是所有的sql执行时间+网络延迟时间+可能的GC,这就是当前执行一行数据的时间。

优化的方向

2.3.2 简单优化

将原本先update(减库存)再进行insert(插入购买明细)的步骤改成:先insert再update。

为什么要先insertupdate

首先是在更新操作的时候给行加锁,插入并不会加锁,如果更新操作在前,那么就需要执行完更新和插入以后事务提交或回滚才释放锁。而如果插入在前,更新在后,那么只有在更新时才会加行锁,之后在更新完以后事务提交或回滚释放锁。

在这里,插入是可以并行的,而更新由于会加行级锁是串行的

也就是说是更新在前加锁和释放锁之间两次的网络延迟和GC,如果插入在前则加锁和释放锁之间只有一次的网络延迟和GC,也就是减少的持有锁的时间。

这里先insert并不是忽略了库存不足的情况,而是因为insert和update是在同一个事务里,光是insert并不一定会提交,只有在update成功才会提交,所以并不会造成过量插入秒杀成功记录。

2.3.3 深度优化

客户端逻辑事务SQLMYSQL端执行,完全屏蔽网络延迟和GCMYSQL只需告诉最终结果。

1. 阿里巴巴做了一个mysql源码层的修改方案,当执行完update之后,它会自动做回滚,回滚的条件影响的记录数是1,就会commit;如果是0就会rollback,不由java客户端来控制commit或者rollback,不给java客户端和mysql之间通信的网络延迟,本质上减低了网络延迟或者GC的干扰,但是这个成本高,要修改mysql源码,只有大公司能做。

2.我们可以将执行秒杀操作时的insert和update放到MySQL服务端的存储过程里,而Java客户端直接调用这个存储过程,这样就可以避免网络延迟和可能发生的GC影响。另外,由于我们使用了存储过程,也就使用不到Spring的事务管理了,因为在存储过程里我们会直接启用一个事务。

2.3.4 优化总结

 

预知后事如何,请看下篇分解:秒杀系统优化方案(下)吐血整理

ASP.NET Web 应用 Docker踩坑历程 - 毛毛虫 - 博客园

mikel阅读(754)

来源: ASP.NET Web 应用 Docker踩坑历程 – 毛毛虫 – 博客园

听说Docker这玩意挺长时间了,新建Web应用的时候,也注意到有个启用Docker的选项。
前两天扫了一眼《【大话云原生】煮饺子与docker、kubernetes之间的关系》,觉得有点意思,决定试试Docker。
然后被坑、百度…
现将整个过程记录一下

一、新建项目

点击创建,被通知“需要安装Docker Desktop”,于是下载、安装、按要求重启电脑。

运行Docker Desktop,被通知“需要安装WSL 2”,于是又下载、安装。

二、运行项目

按 F5 运行项目,vs停在 Info: C:\Users\catzhou\vsdbg\vs2017u5 exists, deleting.不动了。

百度到《visual studio 容器工具首次加载太慢 vsdbg\vs2017u5 exists, deleting 的解决方案》这篇文章,依葫芦画瓢搞定。

再次 F5,成果如下:

吐槽一下:下载vsdbug的两个包速度实在太慢了(为此特意安装了迅雷),然后创建文件夹、4个文本文件颇不容易。俺把vs2017u5打了个包(版本是:17.0.10712.2),你直接下载解压到vsdbg即可。

三、发布到Docker Hub

  1. Docker Hub注册了一个用户
  2. Docker Desktop登录
    登陆后

    变成了
  3. 发布




    点击发布,成果如下:

四、部署到 阿里云-轻量应用服务器-Docker应用镜像

花了60元人民币,买了一个月的Docker应用镜像服务器

  1. 远程连接到服务器
  2. 切换到root账号
    sudo su root
  3. 拉取镜像
    docker pull catzhou2021/webapp1
  4. 查看镜像
    docker images
  5. 创建容器c1并后台运行
    docker run --name=c1 -p 12345:80 -d catzhou2021/webapp1
  6. 查看是否正常运行
    curl http://localhost:12345
  7. 设置防火墙-添加规则

  8. 浏览器访问

如此,大功告成。

PHP判断数组是否为空的常用方法(五种方法)-php教程-PHP中文网

mikel阅读(1739)

来源: PHP判断数组是否为空的常用方法(五种方法)-php教程-PHP中文网

本文介绍了PHP开发中遇到的数组问题,小编在这里给大家总结了5中方法关于php判断数组是否为空问题,需要的朋友参考下
本文介绍了PHP开发中遇到的数组问题,这里介绍了判断PHP数组为空的5种方法,有需要的朋友可以借鉴参考一下。

1. isset功能:判断变量是否被初始化

说明:它并不会判断变量是否为空,并且可以用来判断数组中元素是否被定义过

注意:当使用isset来判断数组元素是否被初始化过时,它的效率比array_key_exists高4倍左右

1

2

3

4

5

6

7

8

9

$a = ”;

$a[‘c’] = ”;

if (!isset($a)) echo ‘$a 未被初始化’ . “”;

if (!isset($b)) echo ‘$b 未被初始化’ . “”;

if (isset($a[‘c’])) echo ‘$a 已经被初始化’ . “”;

// 显示结果为

// $b 未被初始化

// $a 已经被初始化

2. empty功能:检测变量是否为”空”

说明:任何一个未初始化的变量、值为 0 或 false 或 空字符串”” 或 null的变量、空数组、没有任何属性的对象,都将判断为empty==true

注意1:未初始化的变量也能被empty检测为”空”

注意2:empty只能检测变量,而不能检测语句

1

2

3

4

5

6

7

8

$a = 0;

$b = ”;

$c = array();

if (empty($a)) echo ‘$a 为空’ . “”;

if (empty($b)) echo ‘$b 为空’ . “”;

if (empty($c)) echo ‘$c 为空’ . “”;

if (empty($d)) echo ‘$d 为空’ . “”;

3. var == null功能:判断变量是否为”空”

说明:值为 0 或 false 或 空字符串”” 或 null的变量、空数组、都将判断为 null

注意:与empty的显著不同就是:变量未初始化时 var == null 将会报错。

1

2

3

4

5

6

7

8

9

10

$a = 0;

$b = array();

if ($a == null) echo ‘$a 为空’ . “”;

if ($b == null) echo ‘$b 为空’ . “”;

if ($c == null) echo ‘$b 为空’ . “”;

// 显示结果为

// $a 为空

// $b 为空

// Undefined variable: c

4. is_null功能:检测变量是否为”null”

说明:当变量被赋值为”null”时,检测结果为true

注意1:null不区分大小写:$a = null; $a = NULL 没有任何区别

注意2:仅在变量的值为”null”时,检测结果才为true,0、空字符串、false、空数组都检测为false

注意3:变量未初始化时,程序将会报错

1

2

3

4

5

6

7

8

9

$a = null;

$b = false;

if (is_null($a)) echo ‘$a 为NULL’ . “”;

if (is_null($b)) echo ‘$b 为NULL’ . “”;

if (is_null($c)) echo ‘$c 为NULL’ . “”;

// 显示结果为

// $a 为NULL

// Undefined variable: c

5. var === null功能:检测变量是否为”null”,同时变量的类型也必须是”null”

说明:当变量被赋值为”null”时,同时变量的类型也是”null”时,检测结果为true

注意1:在判断为”null”上,全等于和is_null的作用相同

注意2:变量未初始化时,程序将会报错

总结:

PHP中,”NULL” 和 “空” 是2个概念。

isset 主要用来判断变量是否被初始化过

empty 可以将值为 “假”、”空”、”0″、”NULL”、”未初始化” 的变量都判断为TRUE

is_null 仅把值为 “NULL” 的变量判断为TRUE

var == null 把值为 “假”、”空”、”0″、”NULL” 的变量都判断为TRUE

var === null 仅把值为 “NULL” 的变量判断为TRUE

注意:在判断一个变量是否真正为”NULL”时,大多使用 is_null,从而避免”false”、”0″等值的干扰。

以上所述是小编给大家介绍的PHP判断数组是否为空的常用方法(五种方法),希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对PHP中文网的支持!

更多PHP判断数组是否为空的常用方法(五种方法)相关文章请关注PHP中文网!

Electron从零开始——介绍 - 知乎

mikel阅读(791)

来源: Electron从零开始——介绍 – 知乎

其实在 Electron 出现之前,如果你问我做桌面应用需要什么,我的第一个想法是 CC++C#Java 以及微软的 Visual Basic 等等语言(Sorry 我不是个苹果党,第一时间想不起 Object-C),外加体积巨大的 IDE(比如我到今天依然不喜欢的 Visual Studio 和曾经为了玩 Minecraft Mod 开发装的 Eclipse 等等),当然也还有各种编辑器 + 编译器的组合,比如 Notepad++  GCC……

我认识 Electron 项目应该是比较晚的了,是因为突然有一天微软发布了 Visual Studio Code 这个跨平台、开源、免费的编辑器,然后在浏览这个应用的文档时,我突然发现这个东东居然用的是 TypeScript + Electron 开发出来的,而 TypeScript 其实就是 JavaScript 的超集……这个冲击了我的世界观,要知道直接使用 JavaScript 开发桌面应用以前不是没有过,但是能够提供像是 VS Code 这么好用且顺滑的感觉,在之前是不敢想象的,甚至为了一个小小的工具,我都要在 VS 里面先做窗口界面,然后用 C# 在那里搞来搞去,可能是对桌面端应用的不熟悉,我做这种工具总是没有很顺手的感觉(对,这方面我的确很渣);而 Electron 的出现,让我有了可以使用自己熟悉的语言以及简单的编辑工具,就可以做出一个小桌面应用的希望。

当然认真学了一阵时间之后,我发现它其实就是把 V8 引擎单独包装起来,使之成为可以在桌面运行的类浏览器平台,从而使得我们写的 SPA 独立运行在系统桌面段;又因为它其实是基于 SPA 的,所以跨平台就成为了其最基础的能力,除了在调用系统功能和最终打包的时候需要考虑到不同平台的差异,其他方面完全不用考虑,使得同一版本的应用在不同系统上也可以保持一致功能,而不需要重新开发一个不同内核、不同细节项、不同技术栈的应用了,对于个人开发者,小团队乃至大型团队的中小型项目,都是个非常非常棒的特性。

我的作品本身其实也很渣,技术层面并没有什么特别的地方,甚至最初的两个小应用,还是用了 JQuery 作为 DOM 操作技术栈,其中一个基本没有在 Github 上面更新过,就单纯的是写出来玩的,一个读书应用(这个就不截图了,太渣自己看了都不好意思);另一个有持续更新一段时间,但是也没有完成,因为最近在大改,从 JQuery 改为 Vue,所以可以看看以前的样子:


这是一个投资理财的辅助分析工具,包括了三个小模块:股票、指数基金以及可转债,因为是辅助分析,所以有些部分是需要自己已经了解理论才能一眼看懂的,其界面如下(没做菜单,因为按钮对于不是很了解电脑的人更直观)。

主界面:

股票模块主界面(图表这里就不截图了,因为在修改,目前不太好看):

股票模块——白马组合(以防有人说我荐股,这里把股票名称和代码隐藏了):

股票模块——便宜组合(关键信息已隐藏):

股票模块——券商股:

指数基金模块(这里主要是行业指数的博格公式,长投温度考虑到可以很容易的获取,所以没有做进来):

点击查看博格公式,可以看到计算结果:

因为上述这些都是共有的信息,而且无论是否明白博格公式,我们也不可能参照旧数据来进行投资,所以这里没有隐藏;这里应用做的事情,其实就是简单的把需要手工计算的东西自动计算并汇总,所以没什么技术含量的。

可转债模块——可转债组合(关键信息已隐藏):

行情简表和待发转债按钮会直接打开集思录:

关于界面:

以上就是我这个简单的小工具的演示,当然或许做成移动端更好,毕竟现在使用电脑进行投资的人不多了,不过在实际使用的过程中,我学习到的投资理财工具有很多地方还是需要计算和分析,使用电脑明显优于移动端的体验,所以第一步先做了桌面端的应用;外加我现在算是转了行,而且最近正在培训阶段,精力也不太好分配,所以改动技术栈还是需要很久的(一天可能就只剩下了2个小时左右的闲暇,我还要更新喜马拉雅上面的有声小说,所以只有周末是比较好的空暇时间了),这样我打算和这个专栏一样,慢慢的来,正好可以重新梳理自己对 Electron 的理解,并且让自己的应用“升级”为正式版。

好了,这篇文章少见的扯了一大堆废话,接下来先好好的了解一下 Electron 究竟是个什么东东。


就像我上面提到过的,Electron 可以让我们使用纯 JavaScript 调用丰富的原生 API 来创建桌面应用;可以把它看作是专注于桌面应用而不是 web 服务器的 io.js 的一个变体。

当然这不意味着 Electron 是绑定了 GUI 库的 JavaScript;相反,Electron 使用 web 页面作为它的 GUI,所以可以把它看作成一个被 JavaScript 控制的,精简版的 Chromium 浏览器——从这个角度来理解,我们就明白了,所有和系统的交互功能,Electron 这个平台已经搞定了,我们所需要的就是创建一个自己喜欢的界面,一个或多个核心的功能,以及调用它提供给我们的 API 就好。

因为 Electron 本身其实也是 Node.js 的一个第三方项目,所以在开发的时候,我们首先需要已经安装了 Node.js,以及包管理器 NPM,外加一个顺手的编辑器:我个人用 VS Code 很顺手,当然如果喜欢用别的编辑器也没问题,比如前文提到的 Notepad++,或者著名的 Sublime TextAtom(话说这个也是 Electron 做的)、Komodo Edit……等等等等,都可以;基本准备就完成了。

接下来,用 NPM 在本地安装一个 Electron 的副本即可,可以选择全局安装或者针对项目单独安装:

npm i -g electron

# 或者
npm i --save-dev electron
当然我这里因为已经安装过,所以相当于升了个级

一个基础的 Electron 项目结果如下:

my-electron-app/
├── package.json
├── main.js
└── index.html

因为 Node.js 对项目结构以及命名还是有点要求的,所以我个人推荐用一些 Electron Cli 工具来进行项目启动和初始化,这样会相对省一些事,比如我之前使用过的 electron-forge,或者直接搭配 Vue 来建立的 electron-vue 都可以;具体的 cli 工具我们可以按照自己的喜好选择,如果更倾向用原生 JS 或者想要自己指定框架的,electron-forge 是个好选择:

我们也可以注意到,因为它是用了很多比较老的第三方库,所以有不少警告信息

这里稍稍演示一下通过 electron-forge 来建立新项目的过程,其实很简单,只要新建一个文件夹,定位到文件夹之后通过 init 命令指定当前文件夹为新项目:

electron-forge init

此时 electron-forge 会自动检查当前环境,并安装对应的模块:

这里要留意,如果 NPM 的映像是指向淘宝的话,整个过程会快很多。

完成后,初始配置就结束了:

此时,我们就可以打开 src 目录下的 index.js 来查看初始代码了:

import { app, BrowserWindow } from 'electron';

// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (require('electron-squirrel-startup')) { // eslint-disable-line global-require
  app.quit();
}

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow;

const createWindow = () => {
  // Create the browser window.
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
  });

  // and load the index.html of the app.
  mainWindow.loadURL(`file://${__dirname}/index.html`);

  // Open the DevTools.
  mainWindow.webContents.openDevTools();

  // Emitted when the window is closed.
  mainWindow.on('closed', () => {
    // Dereference the window object, usually you would store windows
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    mainWindow = null;
  });
};

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow);

// Quit when all windows are closed.
app.on('window-all-closed', () => {
  // On OS X it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

app.on('activate', () => {
  // On OS X it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (mainWindow === null) {
    createWindow();
  }
});

// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and import them here.

这样我们就可以在这些初始代码上面,进行自己想要的修改了,当然我们下一章节会开始看看,这些基础的代码究竟是个什么意思;不过目前来看,应该已经很清晰了,毕竟 electron-forge 生成的初始代码,已经有了详尽的注释帮助我们理解了。

其实这个初始代码已经可以运行了,只要运行如下命令:

electron-forge start

可以看到,初始的界面是包括了菜单栏、主界面以及控制台的,而控制台的出现就分明体现了它本质上是个浏览器的特点,这也使得我们在本地及时的进行调整、修复和新功能测试变得非常简单。

PHP调用阿里云短信接口报错的解决 - 童年的回忆 - 博客园

mikel阅读(1186)

来源: PHP调用阿里云短信接口报错的解决 – 童年的回忆 – 博客园

调用短信接口错误如下:

cURL error 60: SSL certificate problem: unable to get local issuer certificate (see https://curl.haxx.se/libcurl/c/libcurl-errors.html) for https://dysmsapi.aliyuncs.com/?Action=SendSms&Format=json&Version=2017-05-25&Timestamp=2021-07-31T03%3A45%3A33Z&…….

这是因为没有配置信任的服务器HTTPS验证。默认情况下,cURL被设为不信任任何CAs,因此浏览器无法通过HTTPs访问你服务器。

到下面站点下最新的pem文件

https://curl.se/docs/caextract.html

 

复制这个文件到php的安装目录下

打开php.ini文件,搜索curl.cainfo  去掉前面的#注释   填上该文件的绝对路径,如下图所示:

 

注意:openssl扩展需要开启

重启nginx/Apapche服务器,问题解决

 Android Studio编辑器中间竖线的去除_芸香大官人的博客-CSDN博客_android studio 中间竖线

mikel阅读(819)

来源: (2条消息) Android Studio编辑器中间竖线的去除_芸香大官人的博客-CSDN博客_android studio 中间竖线

题外(多次没能记住竖线的去除,第一次写博客,突然觉得,人类文明得以长存,在于文字的保存。)

本方法基于Android Studio版本:Android Studio 3.3.2
进入【File】=>【Setting】=>【Editor】=>【General】=>【Appearance】
去除勾选“Show hard wrap guide(configured in Code Style options)”
点击【Apply】或者【OK】,如图所示:

————————————————
版权声明:本文为CSDN博主「芸香大官人」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/RoseChilde/article/details/88586684

关于JS的可选参数,该知道的都在这里了 - 简书

mikel阅读(1553)

来源: 关于JS的可选参数,该知道的都在这里了 – 简书

前言

我是一枚土生土长的iOS程序猿,之所以会写关于JS的文章,主要是因为我最近在负责组里的部分运营需求,所以写前端也逐渐比较多,于是乎学习JS势在必行,所以就开写了。

正文

我们都知道函数是js里的一等公民,并且在js里,你声明一个函数——你可以定制多个参数,与此同时,你在调用该函数的时候不需要传入所有的参数,它就能正常执行——只不过这些参数默认就是undefined而已。所以似乎js的函数天生就带有可选参数这个功能,只不过在你不进行定制的时候它们都具有一个“统一”的值罢了。
所以,我们也知道,除非函数里就实现了针对某个参数为undefined时的行为,让参数为undefined是比较危险的。

该如何实现函数内的可选参数,我们将用js里的构造方法来举例(假如我们要实现一个Person类):

function Person(name, age, height, weight) {
    this.name = name;
    this.age = age;
    this.height = height;
    this.weight = weight;
}

通常我们都会这么实现,现在假想我们要将heightweight参数设为可选参数,可选参数的实质就是令未被赋值的参数具有一个默认值,直白地处理,我们可以写成这样:

function Person(name, age, height, weight) {
    var nHeight = height || 0;
    var nWeight = weight || 0; 
    this.name = name;
    this.age = age;
    this.height = nHeight ;
    this.weight = nWeight ;
}

但是因为我们这里的参数只是简单的赋值给属性,所以我们可以这么写:

function Person(name, age, height, weight) {
    this.name = name;
    this.age = age;
    this.height = height || 0;
    this.weight = weight || 0;
}

面对这样的实现,不难发现它还有点问题——这种实现永远只能把可选参数连续地声明在函数的末端,必要的参数必须得放前面,因为它只能这样生成:

var person = new Person("Turtle", 23);

假如我是ageweight为可选参数呢?

function Person(name, age, height, weight) {
    this.name = name;
    this.age = age || 0;
    this.height = height;
    this.weight = weight || 0;
}
// 我就不能这样生成Person对象了
var person = new Person("Turtle", "170cm");

因为这样子赋值,没法让170cm赋到height属性上,只会赋到age属性上,这显然不是我们想要的。

一种简便的解决方法是不定义这么多的参数赋值,而统一使用一个对象来进行赋值:

function Person(options) {
    this.name = options.name;
    this.age = options.age || 0;
    this.height = options.height;
    this.weight = options.weight || 0;
}

var options = {
    name: "Turtle",
    height: "170cm",
};
var person = new Person(options);
//or
var person2 = new Person({
    name: "Turtle",
    height: "170cm",
});

而在es6里,它支持了为参数提供默认值,所以你可以这么干:

function Person({name, age = 0, height, weight = 0} = {}) {
    this.name = name;
    this.age = age;
    this.height = height;
    this.weight = weight;
}
// 效果和上面一致

作者:turtleeeee
链接:https://www.jianshu.com/p/55fc2be7e0f0
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 tp中U方法在传值变量时的运用_chevinswift的博客-CSDN博客

mikel阅读(750)

来源: (3条消息) tp中U方法在传值变量时的运用_chevinswift的博客-CSDN博客

U方法用于完成对URL地址的组装,特点在于可以自动根据当前的URL模式和设置生成对应的URL地址,格式为:

U(‘地址’,’参数’,’伪静态’,’是否跳转’,’显示域名’);

1 //比如操作成功跳转到Store模块下的Ump控制器中的lists方法
2 $this->success('新增成功',U('Strore/Ump/lists'));
1 //跳转时带着参数的话
2 $this->success('新增成功',U('Store/Ump/lists','type=1&id=1'));

当在模板中使用U方法时,好处在于:一旦你的环境变化或者参数设置改变,你不需要更改模板中的任何代码。

在模板中的调用格式需要采用 {:U(‘地址’, ‘参数’…)} 的方式

<!--在模板中使用U方法 -->
{:U('Store/Ump/lists','type=1&id=1')}
{:U('Article/index','category='.$vo['name'])}

 

有一点重要的那就是传变量值时,例如{$news.id}

 

  1. <volist name=“news” id=“news”>
  2. <ul>
  3. <li class=“news_li”>
  4. <a href=“{:U(‘News/news_detail’,’id=’.$news[‘id’])}”>
  1. <span class=“news_content”>{$news.content}</span></a>
  2. <span class=“news_time”>{$news.time}</span>
  3. </li>
  4. </ul>
  5. </volist>

重点在于传值时要把变量通过”.”+变量的索引来写