项目黑马点评
FANSEA前端nginx项目启动

nginx重新加载配置文件
session实现短信验证

redis实现短信验证

- 发送验证码存入redis
- 验证成功登录,并以token为键,保存用户数据,把token返回前端
- 在拦截器中对token进行校验获取用户信息来显示登录状态
库存减一的方法
1 2 3
| boolean success = seckillVoucherService.update() .setSql("stock = stock-1") .eq("voucher_id", voucherId).update();
|
乐观锁解决高并发
CAS法
添加版本号version,简化以库存为版本号

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

在每次减库存的时候都判断当时的库存是否大于0
悲观锁实现一人一单
//synchronized (字符串){代码块}设置悲观锁,此时多个线程中只有首先获的userId这把锁的才能执行事务
1 2 3 4 5
| synchronized (userId.toString().intern()){ IVoucherOrderService proxy =(IVoucherOrderService) AopContext.currentProxy(); return proxy.createVoucherOrder(voucherId); }
|
代理对象的配置
- 导入aspectj依赖
1 2 3 4
| <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency>
|
- 启动类加上注解@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("用户已购买一单!"); } boolean success = seckillVoucherService.update() .setSql("stock = stock-1") .eq("voucher_id", voucherId) .gt("stock",0).update(); if (!success){ return Result.fail("库存不足"); }
VoucherOrder voucherOrder = new VoucherOrder(); long orderId = redisIdWorker.nextId("order"); voucherOrder.setVoucherId(voucherId); voucherOrder.setId(orderId); voucherOrder.setUserId(userId); save(voucherOrder); return Result.ok(orderId); }
|
IDEA配置集群

重新加载nignx的配置文件

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

分布式锁解决集群上锁问题
分布式锁:满足分布式式系统或集群模式下多进程可见并且互斥的锁。
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); }
|

LUA脚本实现原子性
Lua脚本保障原子性:其实就是解决了两段代码之间可能会发生的JVM阻塞问题,解决这个问题就会防止锁超时释放而程序而因为释放锁线程阻塞导致新线程获取到的锁被其他线程释放掉了、这会导致资源不同步
问题:释放锁的代码中有多行代码,多行代码有概率产生jvm阻塞,导致判断当前锁是自己的但是由于超时原因误删了别人的锁,这个时候需要想办法使用一段代码将判断标识和删除锁 ( 相当于一段代码 ) 的动作同时执行!此时Lua横空出世,说了声:交给我吧
1 2 3 4 5 6 7
| if(threadId == lock.threadId){ unlock(); }else{ return; }
|

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"
|

一、Lua脚本实现线程标识判断和释放锁统一的动作
1 2 3 4 5 6 7
| if(redis.call('get',KEY[1]) ==ARGV[1]) then return redis.call('del',KEY[1]) end
return 0
|
二、java代码调用Lua脚本实现原子性
- 初始化加载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 2 3 4 5 6 7 8
| public void unLock() { 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
| local seckillId = ARGV[1] local userId =ARGV[2] local orderId = ARGV[3]
local seckillKey ='seckill:stock:'..seckillId local orderKey ='seckill:order:'..seckillId
if(tonumber(redis.call('get',seckillKey)) <= 0) then return 1 end
if(redis.call('sismember',orderKey,userId) == 1 ) then return 2 end
redis.call('incrby',seckillKey,-1)
redis.call('sadd',orderKey,userId)
redis.call('xadd','stream.orders','*','userId',userId,'voucherId',seckillId,'id',orderId) return 0
|
Redisson—-Redis分布式锁优化
- leaseTime:这是指锁自动释放的时间
- waitTime:这是指客户端尝试获取锁的最大等待时间

一、配置Redission
- 引入依赖
1 2 3 4 5 6
| <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.13.6</version> </dependency>
|
- 引入配置类
1 2 3 4 5 6 7 8 9 10
| @Configuration public class RedisConfig {
@Bean public RedissonClient redissonClient(){ Config config = new Config(); config.useSingleServer().setAddress("127.0.0.1:6379"); return Redisson.create(config); } }
|
二、使用Redisson
- 获取锁对象,获取锁
1 2
| RLock redisLock = redissonClient.getLock("lock:order:" + userId); boolean success = redisLock.tryLock();
|
- 释放锁
1
| redisLock.unlock(重复获取锁等待时间,锁释放时间,时间单位);
|
Redisson原理总结

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


RedissonMultiLock解决分布式主从一致问题
- 配置多个分布式锁
- 创建Multilock
后续使用此锁,锁对象会创建在各个服务器的Redis里,解决主从不一致的问题,多层保护,防止一台Redis宕机,造成其他线程获取到锁造成安全隐患
Redis消息队列优化性能
redis stream 实现消息队列_streammessagelistenercontainer-CSDN博客
- 新增秒杀券的同时,将优惠券信息保存到Redis
- 基于Lua脚本,判断秒杀库存,一人一单,决定用户是否抢购成功
- 抢购成功,将优惠券id和用户id封装存入阻塞队列
- 开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能
基于Redis的Stream实现消息队列

- 创建阻塞队列和消费组
1 2 3
| # 所以,这个命令的整体意思是:在Redis中,如果stream.orders这个流不存在,则创建它,并在其上创建一个名为g1的消费者组,该消费者组从流的开始处(即ID为0的位置)读取消息。如果stream.orders流已经存在,但你试图在不使用MKSTREAM选项的情况下从流的开始处创建消费者组,Redis会抛出一个错误。
XGROUP create stream.orders g1 0 MKSTREAM
|
不需要自己创建消费者,因为当需要消费者消费时系统会自动创建消费者
- 判断一人一单,和是否在抢购时间确定是否有抢购资格,有资格直接返回订单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
| local seckillId = ARGV[1] local userId =ARGV[2] local orderId = ARGV[3]
local seckillKey ='seckill:stock:'..seckillId local orderKey ='seckill:order:'..seckillId
if(tonumber(redis.call('get',seckillKey)) <= 0) then return 1 end
if(redis.call('sismeber',orderKey,userId) == 1 ) then return 2 end
redis.call('incrby',seckillKey)
redis.call('sadd',orderKey,userId)
redis.call('xadd','stream.orders','*','userId',userId,'voucherId',seckillId,'id',orderId) return 0
|
- java代码读取依次循环读取队列消息,然后创建订单,报错就在try catch里面处理pending-list消息

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 { 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()) ); if (list==null) { continue; } MapRecord<String, Object, Object> record = list.get(0); Map<Object, Object> value = record.getValue(); VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true); createVoucherOrder(voucherOrder); stringRedisTemplate.opsForStream().acknowledge(queueName,"g1",record.getId()); }catch (Exception e){ log.error("出现订单异常",e); handlePendingList(); } } }
private void handlePendingList() { while (true){ try { List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read( Consumer.from("g1", "c1"), StreamReadOptions.empty().count(1), StreamOffset.create(queueName, ReadOffset.from("0")) ); if (list==null || list.isEmpty()) { break; } MapRecord<String, Object, Object> record = list.get(0); Map<Object, Object> value = record.getValue(); VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true); createVoucherOrder(voucherOrder); stringRedisTemplate.opsForStream().acknowledge(queueName,"g1",record.getId()); }catch (Exception e){ log.error("出现pending-list订单异常",e); } } }
|
将链表每个内容补充

实现点赞功能

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) { Long userId = UserHolder.getUser().getId(); Double score = stringRedisTemplate .opsForZSet().score(RedisConstants.BLOG_LIKED_KEY + id, userId.toString()); if (score ==null){ 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 { 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(); }
|
关注与取关
- 创建follow表,包含用户id和关注对象id,创建时间

- 点击关注就保存在表中,取关就删除数据
共同关注
SortedSet
的交集方法
- 改进关注功能:在关注之后,将用户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) { Long userId = UserHolder.getUser().getId(); String key = "follower:"+userId; 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(); }
|
- 查询用户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); }
|

Feed流实现关注用户消息推送
本项目采用按时间排序
- 拉模式:用户查看邮件才拉取关注的人的信息,响应慢
- 推模式(写扩散):响应快,但是内存占用高
- 推拉结合:
- 推:普通人消息、大V消息的普通粉丝
- 拉:适用于大V消息(将粉丝分为普通粉丝和活跃粉丝)活跃粉丝拉


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

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("新增文章失败!"); } List<Follow> fans = followService.query().eq("follow_user_id", userId).list(); List<Long> fansId = fans.stream().map(follow -> follow.getUserId()).collect(Collectors.toList()); for (Long fanId : fansId){ String key = "mail:" + fanId; stringRedisTemplate.opsForZSet().add(key,blog.getId().toString(),System.currentTimeMillis()); } return Result.ok(blog.getId()); }
|
用户查询邮箱,获取所有关注的博主的博客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) { Long userId = UserHolder.getUser().getId(); String key = "mail:" + userId; Set<ZSetOperations.TypedTuple<String>> blogSet = stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(key, 0, max, offset, 2); 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); }
|

GEO地理坐标


- 先将所有的店铺经纬度信息按照不同类型存入Redis
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Test void loadShopData(){ 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解决用户签到数据保存

- 将签到数据保存到Redis
1 2 3 4 5 6 7 8 9 10 11 12 13
| public Result sign() { Long userId = UserHolder.getUser().getId(); LocalDateTime now = LocalDateTime.now(); String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM")); int dayOfMonth = now.getDayOfMonth(); String key = SystemConstants.USER_SIGN + userId + keySuffix; stringRedisTemplate.opsForValue().setBit(key,dayOfMonth-1,true); return Result.ok(); }
|
- 获取连续签到天数
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() { Long userId = UserHolder.getUser().getId(); LocalDateTime now = LocalDateTime.now(); String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM")); int dayOfMonth = now.getDayOfMonth(); 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; while (true){ if ((num & 1) == 0) { break; }else { count++; } num >>>= 1; } return Result.ok(count);
}
|
HyperLoglog——用于统计网站访问量的数据结构
PFMERGE用来合并两个key计算总共的统计量

swagger配置
- 引入依赖
注意这里有个大坑,不同springboot版本适配不同swagger!
1 2 3 4 5 6 7 8 9 10 11
| <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 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); }
|
- 配置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/");
}
|
- 配置swagger
1 2 3 4 5
| @Configuration @EnableSwagger2 public class SwaggerConfig {
}
|
- 使用注解完善页面内容
@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 2 3 4 5 6 7
| <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>3.17</version> </dependency>
|
- 获取Excel模板
1
| FileInputStream in = new FileInputStream("D:\\桌面\\Code\\dianping\\hm-dianping\\src\\main\\resources\\template\\shopList.xlsx");
|
- 在对应单元格填入数据
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();
|