黑马点评

黑马点评

前端nginx项目启动

image-20230902202754208

nginx重新加载配置文件

1
nginx.exe reload

session实现短信验证

image-20230711132813197

redis实现短信验证

image-20230714142156437

  1. 发送验证码存入redis
  2. 验证成功登录,并以token为键,保存用户数据,把token返回前端
  3. 在拦截器中对token进行校验获取用户信息来显示登录状态

库存减一的方法

1
2
3
boolean success = seckillVoucherService.update()
.setSql("stock = stock-1")
.eq("voucher_id", voucherId).update();

乐观锁解决高并发

CAS法

添加版本号version,简化以库存为版本号

image-20230904150231750

弊端:

多线程并执行,库存安全问题虽然会解决,但是购买失败率大大提高,扩大了业务上的安全问题,并且多次访问数据库压力巨大

改进:

image-20230904151141861

在每次减库存的时候都判断当时的库存是否大于0

悲观锁实现一人一单

//synchronized (字符串){代码块}设置悲观锁,此时多个线程中只有首先获的userId这把锁的才能执行事务

1
2
3
4
5
synchronized (userId.toString().intern()){
//只有spring管理的对象才能控制事务,这里所以需要使用代理对象,还需要做代理对象的配置,见下文
IVoucherOrderService proxy =(IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}

代理对象的配置

  1. 导入aspectj依赖
1
2
3
4
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
  1. 启动类加上注解@EnableAspectJAutoProxy(exposeProxy = true)
1
2
3
4
5
6
7
8
9
10
@EnableAspectJAutoProxy(exposeProxy = true)
@MapperScan("com.hmdp.mapper")
@SpringBootApplication
public class HmDianPingApplication {

public static void main(String[] args) {
SpringApplication.run(HmDianPingApplication.class, args);
}

}

//此方法实现判断是否已有用户订单的一人一单的秒杀券,作为事务,同时失败同时成功

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
@Transactional
public Result createVoucherOrder(Long voucherId) {
Long userId = UserHolder.getUser().getId();
Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count>0){
return Result.fail("用户已购买一单!");
}
//4,减少库存,创建订单,生成订单id返回
boolean success = seckillVoucherService.update()
.setSql("stock = stock-1")
.eq("voucher_id", voucherId)
.gt("stock",0).update();
if (!success){
return Result.fail("库存不足");
}
/*
id:订单id
userId:用户id
voucherId:优惠券id
*/
VoucherOrder voucherOrder = new VoucherOrder();
//生成全局订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setVoucherId(voucherId);
voucherOrder.setId(orderId);
voucherOrder.setUserId(userId);
save(voucherOrder);
return Result.ok(orderId);
}

IDEA配置集群

image-20230905160922950

image-20230907094435429

重新加载nignx的配置文件

image-20230905161227430

1
2
3
4
//关闭nginx程序
taskkill /IM nginx.exe /F
//重启NGINX
start nginx

image-20230908084824897

分布式锁解决集群上锁问题

分布式锁:满足分布式式系统或集群模式下多进程可见并且互斥的锁。

1
2
3
4
5
6
7
8
private boolean tryLock(String key) {
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(aBoolean);
}

private void unLock(String key) {
stringRedisTemplate.delete(key);
}

image-20230905193034114

LUA脚本实现原子性

Lua脚本保障原子性:其实就是解决了两段代码之间可能会发生的JVM阻塞问题,解决这个问题就会防止锁超时释放而程序而因为释放锁线程阻塞导致新线程获取到的锁被其他线程释放掉了、这会导致资源不同步

问题:释放锁的代码中有多行代码,多行代码有概率产生jvm阻塞,导致判断当前锁是自己的但是由于超时原因误删了别人的锁,这个时候需要想办法使用一段代码将判断标识和删除锁 ( 相当于一段代码 ) 的动作同时执行!此时Lua横空出世,说了声:交给我吧

1
2
3
4
5
6
7
if(threadId == lock.threadId){
//JVM阻塞..................超时释放!
//线程这时候就会把别人获取到的锁释放了!
unlock();
}else{
return;
}

image-20230909135545001

1
2
3
4
5
6
7
8
Redis5.0:0>EVAL "return redis.call('set','name','java')" 0
"OK"
Redis5.0:0>get name
"java"
Redis5.0:0>EVAL "return redis.call('set',KEYS[1],ARGV[1])" 1 name jvav
"OK"
Redis5.0:0>get name
"jvav"

image-20230909143307348

一、Lua脚本实现线程标识判断和释放锁统一的动作

1
2
3
4
5
6
7
-- 将unlock()方法变成Lua脚本,这里的KEY[1]为key,ARGV[1]为传入的当前的线程标识 
if(redis.call('get',KEY[1]) ==ARGV[1]) then
--释放锁
return redis.call('del',KEY[1])
end
-- flase
return 0

二、java代码调用Lua脚本实现原子性

  1. 初始化加载Lua脚本转化为DefaultRedisScript类
1
2
3
4
5
6
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("redisUnlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
  1. 调用脚本
1
2
3
4
5
6
7
8
public void unLock() {
//以免业务阻塞时,查询线程id是否一致,错删别人的锁以至于一人多单
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX+name),
THREAD_PREFIX +Thread.currentThread().getId()
);
}

直接生成单元素集合

1
Collections.singletonList(元素)

下单原子性:

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
--传入优惠券id,用户id
local seckillId = ARGV[1]
local userId =ARGV[2]
local orderId = ARGV[3]

--拼接KEY(1.库存key 2.订单key)
local seckillKey ='seckill:stock:'..seckillId
local orderKey ='seckill:order:'..seckillId

--脚本业务
--1.库存是否充足
if(tonumber(redis.call('get',seckillKey)) <= 0) then
--库存不足返回1
return 1
end
--2.一人是否一单
if(redis.call('sismember',orderKey,userId) == 1 ) then
--存在,说明重复下单,返回2
return 2
end
--3.扣减库存 incrby seckillKey -1
redis.call('incrby',seckillKey,-1)
--4.保存用户id在order集合
redis.call('sadd',orderKey,userId)
--将数据添加到阻塞队列中(可以替换成MQ),监听器执行数据库库存扣除操作
redis.call('xadd','stream.orders','*','userId',userId,'voucherId',seckillId,'id',orderId)
return 0

Redisson—-Redis分布式锁优化

  • leaseTime:这是指锁自动释放的时间
  • waitTime:这是指客户端尝试获取锁的最大等待时间

image-20230910112626390

一、配置Redission

  1. 引入依赖
1
2
3
4
5
6
<!--redisson配置-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
  1. 引入配置类
1
2
3
4
5
6
7
8
9
10
@Configuration
public class RedisConfig {
// 配置Redisson
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
config.useSingleServer().setAddress("127.0.0.1:6379");
return Redisson.create(config);
}
}

二、使用Redisson

  1. 获取锁对象,获取锁
1
2
RLock redisLock = redissonClient.getLock("lock:order:" + userId);
boolean success = redisLock.tryLock();
  1. 释放锁
1
redisLock.unlock(重复获取锁等待时间,锁释放时间,时间单位);

Redisson原理总结

image-20241128140253377

  • 可重入:Redisson锁采用了HashMap结构,记录了获取锁的线程id以及重入次数
  • 可重试:发布订阅模式+信号量配合实现重试,这种重试机制不是忙等,不会消耗过高的CPU性能
  • 可续期:全局Map保存看门狗Task,延时执行递归调用,保障锁不会超时被释放

Redisson可重入原理

image-20230911231428729

Redisson底层原理

获取锁Lua脚本

image-20230914230337073

释放锁Lua脚本

image-20230914230436508

image-20230916084420403

image-20230916084703023

RedissonMultiLock解决分布式主从一致问题

  1. 配置多个分布式锁
image-20230916092245804
  1. 创建Multilock
image-20230916092423998

后续使用此锁,锁对象会创建在各个服务器的Redis里,解决主从不一致的问题,多层保护,防止一台Redis宕机,造成其他线程获取到锁造成安全隐患

Redis消息队列优化性能

redis stream 实现消息队列_streammessagelistenercontainer-CSDN博客

image-20230916104204859 image-20230916132250195
  1. 新增秒杀券的同时,将优惠券信息保存到Redis
  2. 基于Lua脚本,判断秒杀库存,一人一单,决定用户是否抢购成功
  3. 抢购成功,将优惠券id和用户id封装存入阻塞队列
  4. 开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能

基于Redis的Stream实现消息队列

image-20230918170909636

  1. 创建阻塞队列和消费组
1
2
3
# 所以,这个命令的整体意思是:在Redis中,如果stream.orders这个流不存在,则创建它,并在其上创建一个名为g1的消费者组,该消费者组从流的开始处(即ID为0的位置)读取消息。如果stream.orders流已经存在,但你试图在不使用MKSTREAM选项的情况下从流的开始处创建消费者组,Redis会抛出一个错误。

XGROUP create stream.orders g1 0 MKSTREAM
image-20230918185754344

不需要自己创建消费者,因为当需要消费者消费时系统会自动创建消费者

  1. 判断一人一单,和是否在抢购时间确定是否有抢购资格,有资格直接返回订单id,将数据发送到消息队列异步处理
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
--传入优惠券id,用户id
local seckillId = ARGV[1]
local userId =ARGV[2]
local orderId = ARGV[3]

--拼接KEY(1.库存key 2.订单key)
local seckillKey ='seckill:stock:'..seckillId
local orderKey ='seckill:order:'..seckillId

--脚本业务
--1.库存是否充足
if(tonumber(redis.call('get',seckillKey)) <= 0) then
--库存不足返回1
return 1
end
--2.一人是否一单
if(redis.call('sismeber',orderKey,userId) == 1 ) then
--存在,说明重复下单,返回2
return 2
end
--3.扣减库存 incrby seckillKey -1
redis.call('incrby',seckillKey)
--4.保存用户id在order集合
redis.call('sadd',orderKey,userId)
--将数据添加到阻塞队列中,开辟一个独立线程去执行下单操做
redis.call('xadd','stream.orders','*','userId',userId,'voucherId',seckillId,'id',orderId)
return 0
  1. java代码读取依次循环读取队列消息,然后创建订单,报错就在try catch里面处理pending-list消息

image-20230918204825251

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
//创建线程池,用与异步处理秒杀订单
private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
@PostConstruct
private void init(){
SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandle());
}
String queueName = "stream.orders";
//异步处理订单的操作
public class VoucherOrderHandle implements Runnable{
//这边实现读取消息队列创建订单的操作
@Override
public void run() {
while (true){
try {
//1.获取消息队列中的订单消息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS stream.orders >
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
Consumer.from("g1", "c1"),
StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
StreamOffset.create(queueName, ReadOffset.lastConsumed())
);
//1.1判断是否获取成功
if (list==null) {
//1.2不成功,说明无消息,重新执行
continue;
}
//解析订单信息
MapRecord<String, Object, Object> record = list.get(0);
Map<Object, Object> value = record.getValue();
VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
//1.3成功,创建订单
createVoucherOrder(voucherOrder);
//2.ACK确认
stringRedisTemplate.opsForStream().acknowledge(queueName,"g1",record.getId());
}catch (Exception e){
log.error("出现订单异常",e);
handlePendingList();
}
}
}

private void handlePendingList() {
while (true){
try {
//1.获取pending—list队列中的订单消息 XREADGROUP GROUP g1 c1 COUNT 1 STREAMS stream.orders 0
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
Consumer.from("g1", "c1"),
StreamReadOptions.empty().count(1),
StreamOffset.create(queueName, ReadOffset.from("0"))
);
//1.1判断是否获取成功
if (list==null || list.isEmpty()) {
//1.2不成功,说明无消息,重新执行
break;
}
//解析订单信息
MapRecord<String, Object, Object> record = list.get(0);
Map<Object, Object> value = record.getValue();
VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
//1.3成功,创建订单
createVoucherOrder(voucherOrder);
//2.ACK确认
stringRedisTemplate.opsForStream().acknowledge(queueName,"g1",record.getId());
}catch (Exception e){
log.error("出现pending-list订单异常",e);
}
}
}

将链表每个内容补充

image-20230919230517150

实现点赞功能

image-20230919231844464

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public Result likeBlog(Long id) {
//更改使用scoreSet,用分数进行排行,然后按顺序返回点赞用户头像
Long userId = UserHolder.getUser().getId();
//判断是否已经点赞
Double score = stringRedisTemplate
.opsForZSet().score(RedisConstants.BLOG_LIKED_KEY + id, userId.toString());
if (score ==null){
//1.如果没点赞,点赞数+1,增加集合
boolean like = update().setSql("liked = liked+1").eq("id", id).update();
if (BooleanUtil.isTrue(like)){
stringRedisTemplate.opsForZSet()
.add(RedisConstants.BLOG_LIKED_KEY+id,userId.toString(),System.currentTimeMillis());
}
}else {
//2.如果已经点赞,则取消点赞-1,并删除集合
boolean ifLike = update().setSql("liked = liked-1").eq("id", id).update();
if (BooleanUtil.isTrue(ifLike)){
stringRedisTemplate.opsForZSet().remove(RedisConstants.BLOG_LIKED_KEY+id,userId.toString());
}
}
return Result.ok();
}

关注与取关

  1. 创建follow表,包含用户id和关注对象id,创建时间

image-20230923131430511

  1. 点击关注就保存在表中,取关就删除数据

共同关注

SortedSet的交集方法

  1. 改进关注功能:在关注之后,将用户id与关注对象id存在Redis的SortedSet
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
public Result isFollowed(Long id) {
Long userId = UserHolder.getUser().getId();
Integer count = query().eq("user_id", userId).eq("follow_user_id", id).count();
return Result.ok(count >0);
}
public Result follow(Long id, Boolean isFollow) {
//1.获取userId
Long userId = UserHolder.getUser().getId();
String key = "follower:"+userId;
//2.判断是否关注
if (isFollow){
//执行关注操作
Follow follow = new Follow();
follow.setFollowUserId(id);
follow.setUserId(userId);
boolean success = save(follow);
if (success){
stringRedisTemplate.opsForSet().add(key,id.toString());
}
}else {
//执行取关操作
boolean success = remove(new QueryWrapper<Follow>().eq("user_id", userId).eq("follow_user_id", id));
if (success){
stringRedisTemplate.opsForSet().remove(key,id.toString());
}
}
return Result.ok();
}
  1. 查询用户ID和主页ID对应的Redis**”follower:”+userId键值对,并求交集stringRedisTemplate.opsForSet().intersect(key, key1);**
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@GetMapping("/common/{id}")
public Result commonFollow(@PathVariable("id") Long id){
//获取当前用户
String key = "follower:"+ UserHolder.getUser().getId();
String key1 = "follower:"+id;
Set<String> ids = stringRedisTemplate.opsForSet().intersect(key, key1);
List<Long> list = ids.stream().map(Long::valueOf).collect(Collectors.toList());
if (list == null || list.isEmpty()){
return Result.ok(Collections.emptyList());
}
List<UserDTO> userDTOS = userService.listByIds(list).stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList());
return Result.ok(userDTOS);
}

image-20230920142004136

Feed流实现关注用户消息推送

本项目采用按时间排序

  • 拉模式:用户查看邮件才拉取关注的人的信息,响应慢
  • 推模式(写扩散):响应快,但是内存占用高
  • 推拉结合:
    • 推:普通人消息、大V消息的普通粉丝
    • 拉:适用于大V消息(将粉丝分为普通粉丝和活跃粉丝)活跃粉丝拉

image-20241128225659694

image-20230921231240002

  1. 在“大V”写下一篇博客之后,将文章的id保存到粉丝的SortedSet邮箱中(Zset支持滚动分页)

    image-20241128232153122

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    @Override
    public Result saveBlog(Blog blog) {
    // 获取登录用户
    Long userId = UserHolder.getUser().getId();
    blog.setUserId(userId);
    // 保存探店博文
    boolean isSuccess = save(blog);
    if (!isSuccess){
    return Result.fail("新增文章失败!");
    }
    //1.查询所有粉丝 select * from tb_follow where follow_user_id = userId
    List<Follow> fans = followService.query().eq("follow_user_id", userId).list();
    List<Long> fansId = fans.stream().map(follow -> follow.getUserId()).collect(Collectors.toList());
    //2.将文章id推送到用户邮箱(实质就是将文章id保存到粉丝的Redis中SortedSet集合中)
    for (Long fanId : fansId){
    String key = "mail:" + fanId;
    stringRedisTemplate.opsForZSet().add(key,blog.getId().toString(),System.currentTimeMillis());
    }
    // 返回id
    return Result.ok(blog.getId());
    }
  2. 用户查询邮箱,获取所有关注的博主的博客id列表,按时间戳Feed流滚动排序解析到用户关注页

1
2
3
4
5
6
7
8
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ScrollResult {
private List<?> list;
private Long minTime;
private Integer offset;
}
1
2
3
4
5
@GetMapping("/of/follow")
public Result queryFollowBlog(
@RequestParam("lastId") Long max,@RequestParam(value = "offset" ,defaultValue = "0") Integer offset){
return blogService.queryFollowBlog(max,offset);
}
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
35
36
37
38
39
40
41
public Result queryFollowBlog(Long max, Integer offset) {
//1.获取用户id
Long userId = UserHolder.getUser().getId();
//2.查询邮件箱
String key = "mail:" + userId;
Set<ZSetOperations.TypedTuple<String>> blogSet =
stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(key, 0, max, offset, 2);
//3.非空判断
if (blogSet == null || blogSet.isEmpty()){
return Result.ok();
}
long minTime = 0;
int os = 1;
List<Long> ids = new ArrayList<>(blogSet.size());
for (ZSetOperations.TypedTuple<String> blogTuple : blogSet){
ids.add(Long.valueOf(blogTuple.getValue()));
long time = blogTuple.getScore().longValue();
if (time == minTime){
os++;
}else {
minTime = time;
os = 1;
}
}
String idStr = StrUtil.join(",", ids);
List<Blog> blogs = query().in("id",ids).last("ORDER BY FIELD(id,"+idStr+")").list();
for (Blog blog : blogs){
isBlogLiked(blog);
}
ScrollResult scrollResult = new ScrollResult(blogs,minTime,os);
return Result.ok(scrollResult);
}
public void isBlogLiked(Blog blog){
UserDTO user = UserHolder.getUser();
if (user ==null){
return;
}
Long userId = user.getId();
Double isLike = stringRedisTemplate.opsForZSet().score(RedisConstants.BLOG_LIKED_KEY + blog.getId(), userId.toString());
blog.setIsLike(isLike !=null);
}

image-20241128230438370

GEO地理坐标

image-20230923151702697

image-20230923163613466

  1. 先将所有的店铺经纬度信息按照不同类型存入Redis
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
void loadShopData(){
//将所有的店铺经纬度信息按照不同类型存入Redis
List<Shop> list = shopService.list();
Map<Long, List<Shop>> map = list.stream().collect(Collectors.groupingBy(Shop::getTypeId));
for (Map.Entry<Long,List<Shop>> entry: map.entrySet()) {
Long typeId = entry.getKey();
String key = "shop:geo:"+ typeId;
List<Shop> value = entry.getValue();
List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>(value.size());
for (Shop shop : value) {
locations.add(new RedisGeoCommands.GeoLocation<>(shop.getId().toString(),new Point(shop.getX(),shop.getY())));
}
stringRedisTemplate.opsForGeo().add(key,locations);
}


}

BitMap解决用户签到数据保存

image-20230924131126304

  1. 将签到数据保存到Redis
1
2
3
4
5
6
7
8
9
10
11
12
13
public Result sign() {
//1.获取用户id
Long userId = UserHolder.getUser().getId();
//2.获取当前年月日,组装成key
LocalDateTime now = LocalDateTime.now();
String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
int dayOfMonth = now.getDayOfMonth();
//拼装成key
String key = SystemConstants.USER_SIGN + userId + keySuffix;
//3.对Redis操作
stringRedisTemplate.opsForValue().setBit(key,dayOfMonth-1,true);
return Result.ok();
}
  1. 获取连续签到天数
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
public Result signCount() {
//1.获取用户和年月日
Long userId = UserHolder.getUser().getId();
LocalDateTime now = LocalDateTime.now();
String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
int dayOfMonth = now.getDayOfMonth();
//2.拼接key,并且查询0到当天的签到量 BITFIELD sign:1010:202309 GET u24 0
String key = SystemConstants.USER_SIGN + userId + keySuffix;
List<Long> result = stringRedisTemplate.opsForValue().bitField(key,
BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0));
if (result == null || result.isEmpty()){
return Result.ok(0);
}
Long num = result.get(0);
if (num == null || num == 0){
return Result.ok(0);
}
int count = 0;
//3.右移运算和与运算获取当前位,是1就加一,是0就返回
while (true){
//相与获取当前位的数 1&1=1 1&0=0
if ((num & 1) == 0) {
break;
}else {
count++;
}
//执行右移操作,使其循环向右读取
num >>>= 1;
}
return Result.ok(count);

}

HyperLoglog——用于统计网站访问量的数据结构

PFMERGE用来合并两个key计算总共的统计量

image-20230924170913733

swagger配置

  1. 引入依赖

注意这里有个大坑,不同springboot版本适配不同swagger!

1
2
3
4
5
6
7
8
9
10
11
<!--        swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
  1. 配置取消拦截
1
2
3
4
5
6
7
8
String[] swaggerExcludes=new String[]{"/swagger-ui.html","/swagger-resources/**","/webjars/**"};

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(swaggerExcludes).order(1);
registry.addInterceptor(new RefreshInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
}
  1. 配置swagger资源映射
1
2
3
4
5
6
7
8
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");

}
  1. 配置swagger
1
2
3
4
5
@Configuration //加入到配置类 springboot声明配置类
@EnableSwagger2 //开启Swagger2
public class SwaggerConfig {

}
  1. 使用注解完善页面内容

@Api 更改controller备注

@ApiOperation 更改方法备注

@ApiImplicitParam 更改方法参数备注

@ApiModel 更改方法参数类备注

@ApiModelProperty更改模型属性备注

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Api(tags = "优惠券",description = "优惠券相关的接口")
public class VoucherController {

@PostMapping
@ApiOperation(value = "添加消费券",notes = "用于商家添加优惠券")
public Result addVoucher(@RequestBody Voucher voucher) {
voucherService.save(voucher);
return Result.ok(voucher.getId());
}

@GetMapping("/list/{shopId}")
@ApiOperation(value = "查询消费券",notes = "用于查询消费券")
@ApiImplicitParam(name = "shopId",value = "消费券id")
public Result queryVoucherOfShop(@PathVariable("shopId") Long shopId) {
return voucherService.queryVoucherOfShop(shopId);
}
}
1
2
3
4
5
@ApiModel(value = "优惠券类")
public class Voucher implements Serializable {
@ApiModelProperty(value = "使用规则")
private String rules;

导出Excel

  1. 引入依赖
1
2
3
4
5
6
7
<!-- POI -->
<!--xlsx(07)-->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.17</version>
</dependency>
  1. 获取Excel模板
1
FileInputStream in = new FileInputStream("D:\\桌面\\Code\\dianping\\hm-dianping\\src\\main\\resources\\template\\shopList.xlsx");
  1. 在对应单元格填入数据
1
2
3
4
5
6
7
8
9
10
11
12
13
XSSFWorkbook excel = new XSSFWorkbook(in);
XSSFSheet sheet = excel.getSheet("Sheet1");
for (int i=1;i<=list.size();i++){
sheet.getRow(i).getCell(0).setCellValue(list.get(i-1).getName());
sheet.getRow(i).getCell(1).setCellValue(list.get(i-1).getAddress());
sheet.getRow(i).getCell(2).setCellValue(list.get(i-1).getAvgPrice());
sheet.getRow(i).getCell(3).setCellValue(list.get(i-1).getScore());
sheet.getRow(i).getCell(4).setCellValue(list.get(i-1).getOpenHours());
}
ServletOutputStream out = response.getOutputStream();
excel.write(out);
out.close();
excel.close();