数据库

img

问题

跨域感知session

跨域感知session需要解决两个问题,第一个是解决跨域问题,第二个是解决跨域cookie传输问题

跨域问题

1.在存在跨域请求问题的类前都加上一句支持跨域操作(服务端解决方式)springboot自带的crossOrigin注解

@CrossOrigin(origins = {“*”},allowCredentials = “true”)

2.,jQuery会有跨域限制,用ajax请求对应网页的url文件位置时,静态资源文件和jQuery动态请求是分离状态,所以在ajax请求要加上一句:

1
xhrFields:{withCredentials:true}

跨域传递cookie问题

@CrossOrigin(origins = {““},allowCredentials = “true”,allowedHeaders = ““)

由于课程中仅仅使用了get和post的方法,而这两个方法在跨域请求中都是可以用的,因此allowedHeaders可以不加。

xhrFields:{withCredentials:true}

基于Redis实现短信登录

验证码存储在session中

主要是session的存储,基于ngnix动态代理,tomcat集群会导致session不一致,因此使用redis存储用户登录信息,

在第一个拦截器preHandle中拦截所有的路径,当注册完成后,用户去登录会去校验用户提交的手机号和验证码,是否一致,如果一致,则根据手机号查询用户信息,不存在则新建,最后将用户数据保存到redis,并且生成token作为redis的key,将其保存到threadLocal中,并且放行,刷新令牌———-afterCompletion中移除用户

第二个拦截器只需要判断ThreadLocal中是否有用户是否存在即可

redis中数据用userdio,保护用户隐私

redis数据结构采用String存储(有内存消耗)/Hash存储方便修改

商户查询缓存

缓存更新策略

image-20221017103715235

数据库缓存不一致解决方案:

Cache Aside Pattern 人工编码方式:缓存调用者在更新完数据库后再去更新缓存,也称之为双写方案

Read/Write Through Pattern : 由系统本身完成,数据库与缓存的问题交由系统本身去处理

Write Behind Caching Pattern :调用者只操作缓存,其他线程去异步处理数据库,实现最终一致

方案一实现:

利用事务先操作数据库再删除缓存

核心思路

修改ShopController中的业务逻辑,满足下面的需求:

根据id查询店铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间

根据id修改店铺时,先修改数据库,再删除缓存

缓存问题

缓存穿透

缓存穿透 :缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。

常见的解决方案有两种:

  • 缓存空对象
    • 优点:实现简单,维护方便
    • 缺点:
      • 额外的内存消耗
      • 可能造成短期的不一致
  • 布隆过滤
    • 优点:内存占用较少,没有多余key
    • 缺点:
      • 实现复杂
      • 存在误判可能

缓存穿透的解决方案有哪些?

  • 缓存null值
  • 布隆过滤
  • 增强id的复杂度,避免被猜测id规律
  • 做好数据的基础格式校验
  • 加强用户权限校验
  • 做好热点参数的限流

核心思路

核心思路如下:

在原来的逻辑中,我们如果发现这个数据在mysql中不存在,直接就返回404了,这样是会存在缓存穿透问题的

现在的逻辑中:如果这个数据不存在,我们不会返回404 ,还是会把这个数据写入到Redis中,并且将value设置为空,当再次发起查询时,我们如果发现命中之后,判断这个value是否是null,如果是null,则是之前写入的数据,证明是缓存穿透数据,如果不是,则直接返回数据

缓存雪崩

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案:

  • 给不同的Key的TTL添加随机值
  • 利用Redis集群提高服务的可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

缓存击穿

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

常见的解决方案有两种:

互斥锁

image-20221017144229327

核心思想

利用redis的setnx方法来表示获取锁,该方法含义是redis中如果没有这个key,则插入成功,返回1,在stringRedisTemplate中返回true, 如果有这个key则插入失败,则返回0,在stringRedisTemplate返回false,我们可以通过true,或者是false,来表示是否有线程成功插入key,成功插入的key的线程我们认为他就是获得到锁的线程。

逻辑过期

image-20221017144236761

核心思想

当用户开始查询redis时,判断是否命中,如果没有命中则直接返回空数据,不查询数据库,而一旦命中后,将value取出,判断value中的过期时间是否满足,如果没有过期,则直接返回redis中的数据,如果过期,则在开启独立线程后直接返回之前的数据,独立线程去重构数据,重构完成后释放互斥锁

在原来基础上封装逻辑过期时间

封装工具类

  • 方法1:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
  • 方法2:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓

存击穿问题

  • 方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
  • 方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题

搜索功能

点评索引创建

一般我们在构建索引的时候是标准最大化分词模式 ,自己想要搜索词用智能模式 , 因为它比较贴近用户的语义, 如果搜索词召回的数据比较少就再用最大化分词, 还是不行就逐字分的模式

1.需要自己先要定义好自己的索引结构、字段类型,分词器模式 : 整数id, 智能化分词的店名, 标签,地理位置信息, 点评分数, 类目类型id, 类目类型名, 商家的分数, 商家启动或者关闭状态。

在这里插入图片描述

2.全量索引构建 : 一般我们使用logstash-input-jdbc中间件去将数据库数据搞到es中 , 和elk中e是一样, 起到数据收集功能。 | 这里面有个问题就是如果sql中在执行事务, logstash-input-jdbc拿到的是快照之前的数据,就会出现数据不一致, 这种是可以接受的, 因为搜索不是很强调内容的过于细节的变动,而且增量数据补偿也可以弥补。 | 还是在es中安装logstash-input-jdbc插件— 填写数据库的配置等, 放入sql语句, sql服务器地址, 放入映射关系

在这里插入图片描述

3.增量(实时修改)索引构建: 增量索引就是通过sql语句去搜索更新日期大于* 的内容,然后对这些数据进行更新。 | 我们也是在es中配置中, 然后在之前的全量索引sql语句中加入时间的判断,当大于自己当前的时间, 更新。 | 到这就实现了刚开始更新全量, 后面实时更新增量的工作, 但是其实这个增量还是比较慢, 而且数据量大的时候会出现更新还没更新完, 还要检测, 结果发现还有很多要更新, 我们后面继续优化。

应用层接入

1.最基本的搜索模型 : 我们根据坐标自定义es语句去计算距离, 然后和搜索的关键字一起定制化打分, 按照分数进行排序 | 这些不同打分权重是根据实际业务去调整的 | 还可以设置关键字只算召回, 排序时候只考虑距离。

2.注意下面是es的工作流程, 一般es搜索过来的内容取到id, 还要去数据库中再找详细的图片等信息的, es只是负责让你方便的搜, 实际搜索的内容需要用户拿到es返回之后请求sql数据库。

在这里插入图片描述

3.java接入 : 一般我们用restful http 去接入es集群 | 先在java项目中加入包, 然后创建配置, 给出http es client 对象, 然后服务端调用http client 服务对象操作数据 | 如下图所示, 这是一个最最简单的es搜索结果

在这里插入图片描述

  1. 复杂排序模型的java封装 : 前面我们简单的java接入是用的高级api, 用函数封装的。 但是我们实际写的搜索模型是比较复杂的, 因此我们复制已经写好的es语句, 替换里面的变量, 以http的get请求发送过去, 最终得到结果。 | 但是这里面的es语句非常复杂, 如果后序要修改非常麻烦, 因为我们直接传入的json文档, 因此我们可以参照mybatis封装sql语句的操作, 以json对象来封装这些底层的语句。 虽然第一次写非常繁琐, 但是后序修改的时候就不用怎么改了, 能够修改部分会加else留空 | 可以看到我们还能根据目录操作进行过滤了, 整体非常流畅 |

    在这里插入图片描述

    1. 将之前用sql写的标签过滤搞成es过滤 , 用来解决无法识别空格的问题。 至此我们的这个搜索模型可以满足大部分需求的, 整个搜索非常强大了。
    2. 额外的改进空间: 分词定制化, 我们需要对凯悦等行业词汇进行专门的定制分词, 对于一些词过滤掉等等 | 相关性 : 对于一些包含语境的词无法理解, 例如我们说休息其实是想搜住宿, 但是我们的带搜索内容中没有休息, 只有和住宿相关的, 而你搜的休息和住宿对于es而言, 没有相关性, 搜不出来。 | 索引实时性: 我们的搜索是一分钟一次, 但是对于高并发的领域太慢了。

搜索相关性的改造

定制化中文词库

扩展专业词库: 创建新的词库, 然后配置到es中使用, 例如凯悦这个词, 单独搞成词组到词库中 , 不过需要注意我们搜索的时候凯悦还是分开的, 我们不能重新删除所有索引再来一次全量更新, 那样太麻烦了, 所以我们可以通过自己通过update by query去创建一个凯悦的搜索分词, 这样可以去找实际存储的凯悦分词对应数据 | 注意这个扩展词库在专业领域非常常用,例如电商汽车等垂直领域都要做大量的这些专业 词修改工作 | 一般我会选热更新词库, 自动更新就不用重启去扩展词库 |

同义词 : 我们在词库中一个词位置写苹果,iphone就能代表同义词。 然后设置指定这个包含同义词的分词器到 商家名等关键字的搜索中, 然后更新增量索引 | 最后我们就可以通过苹果搜出iphone了

重塑相关性

影响召回 : 如果我们搜索住宿, 索引里面没有住宿这个门店, 而住宿更像是一种目录, 因此我们增加目录相关性词, 把住宿和休息等词扩展到词库中连接到酒店这个目录, 然后在搜索的时候增加如果匹配到搜索的词是目录词,就把这个词不用按照name来搜, 按照目录来召回, 这样来看就影响了召回, 也更能理解人的需求 | 影响

影响排序: 将这个相关词的命中了, 写到es的function中去影响排序分数。 | 一般我们的相关词如果影响了召回, 就不太会设置影响排序, 因为根本没有意义, 一般是先开一个如果效果不好的话再开第二个 |

准实时性的改进

canel实时增量更新: 之前我们的input_jdbc在面对大量的数据时候,难以处理, 而且每次都是遍历检查时间, 这样太慢了。 我们使用canel伪装成mysql的从库, 每次数据更新的时候master通知canel具体的更新内容, 然后canel根据通知去将数据放入es中, 这样更为方便。 | 这里面canal-adopter是比较简单的, 因此我们使用java代码实际连接canal去做消费, 然后根据不同的业务数据修改, 做出不同的查询与修改, 并更新到es中, 比如你的sql一个门店数据的修改, 就需要改好多个es的index, 因此这些逻辑还是比较复杂的

优惠卷秒杀

订单表唯一ID

id不能容易被发现规律,容易爆露一些信息

分库分表ID要逻辑唯一

image-20221017145124064

添加优惠卷

普通劵和秒杀卷(添加时增加到Redis)

下单(seckillVoucher)

当用户开始进行下单,我们应当去查询优惠卷信息,查询到优惠卷信息,判断是否满足秒杀条件

比如时间是否充足,如果时间充足,则进一步判断库存是否足够,如果两者都满足,则扣减库存,创建订单,然后返回订单id,如果有一个条件不满足则直接结束。

超卖问题

加锁:

乐观锁和悲观锁

乐观锁:

1.要我扣减库存时的库存和之前我查询到的库存是一样的,就意味着没有人在中间修改过库存,那么此时就是安全的,但是以上这种方式通过测试发现会有很多失败的情况

2.改成stock大于0

1
2
3
boolean success = seckillVoucherService.update()
.setSql("stock= stock -1")
.eq("voucher_id", voucherId).update().gt("stock",0); //where id = ? and stock > 0

一人一单

在seckillVoucher中增加根据优惠卷id和用户id查询是否已经下过这个订单,如果下过这个订单,则不再下单,否则进行下单

并发问题

在方法(createVoucherOrder)上加锁

锁太大

锁住Long userId = UserHolder.getUser().getId();

保证同一把锁,使用synchronized(userId.toString().intern()){

事务问题

事务方法未提交锁会先释放

seckillVoucher 方法中,添加以下逻辑,这样就能保证事务的特性,同时也控制了锁的粒度

image-20221017152406530

事务要使用代理对象调用

image-20221017152436247

分布式锁

多台tomcat服务器锁会不同

使用分布式锁

image-20221017152730970

redis实现分布式锁

  • 利用set nx满足互斥性
  • 利用set ex保证故障时锁依然能释放,避免死锁,提高安全性
  • 利用Redis集群保证高可用和高并发特性

自建类实现ILock接口中的tryLock和unLock方法

我们利用redis 的setNx 方法,当有多个线程进入时,我们就利用该方法,第一个线程进入时,redis 中就有这个key 了,返回了1,如果结果是1,则表示他抢到了锁,那么他去执行业务,然后再删除锁,退出锁逻辑,没有抢到锁的哥们,等待一定时间后重试即可

锁的误删1

持有锁的线程在锁的内部出现了阻塞,导致他的锁自动释放,这时其他线程,线程2来尝试获得锁,就拿到了这把锁,然后线程2在持有锁执行过程中,线程1反应过来,继续执行,而线程1执行过程中,走到了删除锁逻辑,此时就会把本应该属于线程2的锁进行删除,这就是误删别人锁的情况说明

image-20221017153320391

解决办法:

核心逻辑:在存入锁时,放入自己线程的标识,在删除锁时,判断当前这把锁的标识是不是自己存入的,如果是,则进行删除,如果不是,则不进行删除。

锁的误删2

线程1删除锁时,判断完是自己的锁,删除锁之前锁超时释放,线程2拿到锁,线程1把线程2的锁删了

image-20221017153532641

主要是命令不是原子性

解决办法:

Lua脚本

Lua脚本解决多条命令原子性问题
1
2
3
4
5
6
7
8
-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示
-- 获取锁中的标示,判断是否与当前线程标示一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
-- 一致,则删除锁
return redis.call('DEL', KEYS[1])
end
-- 不一致,则直接返回
return 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}

public void unlock() {
// 调用lua脚本
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId());
}
经过以上代码改造后,我们就能够实现 拿锁比锁删锁的原子性动作了~

分布式锁-redission

setnx问题

重入问题,不可重试,超时释放,主从一致性

Redission是什么

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。

Redission提供了分布式锁的多种多样的功能

image-20221017155253688

使用前要在@Configuration+Bean配置

redission可重入锁原理

底层的一个voaltile的一个state变量来记录重入的状态

redission锁的MutiLock原理

把锁加锁的逻辑需要写入到每一个主丛节点上,只有所有的服务器都写入成功,此时才是加锁成功

当我们去设置了多个锁时,redission会将多个锁添加到一个集合中,然后用while循环去不停去尝试拿锁,但是会有一个总共的加锁时间,这个时间是用需要加锁的个数 * 1500ms ,假设有3个锁,那么时间就是4500ms,假设在这4500ms内,所有的锁都加锁成功, 那么此时才算是加锁成功,如果在4500ms有线程加锁失败,则会再次去进行重试.

秒杀优化

1、查询优惠卷

2、判断秒杀库存是否足够

3、查询订单

4、校验是否是一人一单

5、扣减库存

6、创建订单

image-20221017160811292

在Redis中使用判断秒杀库存和校验一人一单,保存信息到阻塞队列,异步完成下单

使用rocketmq处理异步消息

库存售罄标识

使用redis set一个标记

流量削峰

秒杀令牌

验证和下单强关联,要解耦

秒杀大闸

设置一个以秒杀商品初始库存x倍数量作为秒杀大闸,若超出这个数量,则无法发放秒杀令牌

redisTemplate.opsForValue().set(“promo_door_count_”+promoId,itemModel.getStock().intValue()*5);

队列泄洪

  • 排队有些时候比并发更高效

  • 依靠排队去限制并发流量

  • 依靠排队和下游拥塞窗口程度调整队列释放流量大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

private ExecutorService executorService;

@PostConstruct
public void init(){
//定义一个只有20个可工作线程的线程池
executorService = Executors.newFixedThreadPool(20);
}
//同步调用线程池的submit方法
//拥塞窗口为20的等待队列,用来队列化泄洪
Future<Object> future = executorService.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
//加入库存流水init状态
String stockLogId = itemService.initStockLog(itemId,amount);
//再去完成对应的下单事务型消息机制
if(!mqProducer.transactionAsyncReduceStock(userModel.getId(),itemId,promoId,amount,stockLogId)){
throw new BusinessException(EmBusinessError.UNKNOWN_ERROR,"下单失败");
}
return null;
}
});

try {
future.get();
} catch (InterruptedException e) {
throw new BusinessException(EmBusinessError.UNKNOWN_ERROR);
} catch (ExecutionException e) {
throw new BusinessException(EmBusinessError.UNKNOWN_ERROR);
}
return CommonReturnType.create(null);
}


防刷限流

验证码

限流

  • 流量远比你想的要多
  • 系统活着比挂了要好
  • 宁愿只让少数人能用,也不要让所有人不能用

限流方法

限并发

对同一时间固定访问接口的线程数做限制,利用全局计数器,在下单接口OrderController处加一个全局计数器,并支持并发操作,当controller在入口的时候,计数器减1,判断计数器是否大于0,在出口时计数器加一,就可以控制同一时间访问的固定。

令牌桶算法

img

令牌桶算法可以做到客户端一秒访问10个流量,下一秒就是下一个10个流量,限定某个时刻的最大值

漏桶算法

img

漏桶算法的目的用来平滑网络流量,没有办法应对突发流量

附近的商户

批量写入locations

根据距离排序查询,截取距离

用户签到

Redis中是利用string类型数据结构实现BitMap

image-20221017193820343

Redis中是利用string类型数据结构实现BitMap

把年和月作为bitMap的key,然后保存到一个bitMap中,每次签到就到对应的位上把数字从0变成1,只要对应是1,就表明说明这一天已经签到了,反之则没有签到

签到统计

什么叫做连续签到天数?

image-20221017195406010

如何得到本月到今天为止的所有签到数据?

BITFIELD key GET u[dayOfMonth] 0

如何从后向前遍历每个bit位?

num >>>= 1;

和1&运算,1签到,0未签到

hash思想

id % bitmap.size = 算出当前这个id对应应该落在bitmap的哪个索引上,然后将这个值从0变成1,然后当用户来查询数据时,此时已经没有了list,让用户用他查询的id去用相同的哈希算法, 算出来当前这个id应当落在bitmap的哪一位,然后判断这一位是0,还是1,如果是0则表明这一位上的数据一定不存在, 采用这种方式来处理,需要重点考虑一个事情,就是误差率,所谓的误差率就是指当发生哈希冲突的时候,产生的误差。

image-20221017195935373

好友关注

表tb_follow

image-20221017163730702

  • 关注和取关接口
  • 判断是否关注的接口

共同关注

关注时使用放入关注set集合,取关时从set移除

利用set的求交集api查找共同关注

Feed流实现方案

Feed流产品有两种常见模式:

Timeline:不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注。例如朋友圈

智能排序:利用智能算法屏蔽掉违规的、用户不感兴趣的内容。推送用户感兴趣信息来吸引用户

Timeline模式实现方案有三种:

  • 拉模式
  • 推模式
  • 推拉结合

推送消息到收件箱

数据结构sortset

就是我们在保存完探店笔记后,获得到当前笔记的粉丝(查数据库),然后把数据推送到粉丝的redis中去。

stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis())

实现分页查询收件箱

从最大值开始查,查几条,不关心最小值

tringRedisTemplate.opsForZSet() .reverseRangeByScoreWithScores(key, 0, max, offset, 2);

key:

0:最小值

max:当前时间戳||lastid

offset;与lastid相同元素的个数

2:取几条

倒序返回固定条数记录,将id放入数组,计算offset,设置mintime,相同os++,不同重置

查询数据库时in字段查询无序,要使用order by fileld

达人探店

发布探店笔记

tb_blog:探店笔记表,包含笔记中的标题、文字、图片等
tb_blog_comments:其他用户对探店笔记的评价

查看探店笔记

点赞功能

使用Redis的set集合

  • 给Blog类中添加一个isLike字段,标示是否被当前用户点赞
  • 修改点赞功能,利用Redis的set集合判断是否点赞过,未点赞过则点赞数+1,已点赞过则点赞数-1
  • 修改根据id查询Blog的业务,判断当前登录用户是否点赞过,赋值给isLike字段
  • 修改分页查询Blog业务,判断当前登录用户是否点赞过,赋值给isLike字段

点赞排行榜

使用sortedSet

使用// 1.查询top5的点赞用户 zrange key 0 4
Set top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);

再从数据库查找相关用户

UV统计

Redis中的HLL是基于string结构实现的,单个HLL的内存永远小于16kb内存占用低的令人发指!作为代价,其测量结果是概率性的,有小于0.81%的误差。不过对于UV统计来说,这完全可以忽略。

  • UV:全称Unique Visitor,也叫独立访客量,是指通过互联网访问、浏览这个网页的自然人。1天内同一个用户多次访问该网站,只记录1次。
  • PV:全称Page View,也叫页面访问量或点击量,用户每访问网站的一个页面,记录1次PV,用户多次打开页面,则记录多次PV。往往用来衡量网站的流量。

使用HyperLogLog统计进UV数

image-20221017200311746

异常处理

nginx反向代理/OpenResty

  • 设置upstream server
  • 设置动态请求location为proxy pass 路径

img

img

Nginx高性能原因

epoll多路复用

master worker进程模型

img

协程机制

  • 依附于线程的内存模型,切换开销小

一个线程可以有多个协程,不需要cpu切换的开销,只要内存切换。

  • 遇阻塞及归还执行权,代码同步
  • 无需加锁

多级缓存

nginx

  1. 依靠文件系统存索引级的文件(将请求存成本地文件,在本地磁盘中)
  2. 依靠内存缓存文件地址 key中的value存文件地址,
1
2
3
4
5
6
7
8
9
10
11
12
13
#声明一个cache缓存节点的内容
proxy_cache_path /usr/local/openresty/nginx/tmp_cache levels=1:2 keys_zone=tmp_cache:100m inactive=7d max_size=10g;

//做一个二级目录,先将对应的url做一次hash,取最后一位做一个文件目录的索引;
//在取一位做第二级目录的索引来完成对应的操作,文件内容分散到多个目录,减少寻址的消耗
//在nginx内存当中,开了100m大小的空间用来存储keys_zone中的所有的key
//文件存取7天,文件系统组多存取10个G

location / {
proxy_cache tmp_cache;
proxy_cache_key &uri;
proxy_cache_valid 200 206 304 302 7d;//只有后端返回的状态码是这些,对应的cache操作才会生效,缓存周期7天
}

shared dic:共享内存字典,所有worker进程可见

1
2
3
4
5
6
lua_shared_dict my_cache 128m;

location /luaitem/get {
default_type "application/json";
content_by_lua_file ../lua/itemsharedic.lua;
}

页面静态化