news 2026/4/3 4:59:50

白菜代售 案例

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
白菜代售 案例

ps年
title: 优惠卷发布平台
author: John Doe
date: 2024-10-03 20:22:30
categories:

  • 后端实战

ps
自己和同学写的个项目的开发日志,算是回忆吧,代码很青涩

优惠卷发布平台
1.提供登录注册
登录注册采用redis缓存 减缓mysql压力
2.登录验证
采用cookie 对ID进行aes加密,拦截器解密
3.异常捕获
未知异常—全部补获—发送邮箱提醒开发者
4.优惠卷定时发布
采用xll—job
5.优惠券发布
mysql redis各一份
6.抢优惠卷

lua-redis-ecle

7.优惠卷同步

同步
xxl—job 配合线程池异步同步8.使用优惠券
redis 分布式锁 事务问题
9.优惠券展示
redis缓存问题—mysql索引优化

设计思路-

数据库 id 账号 密码

逻辑 -判断账号密码-给与seioon-拦截器进行判断

技术方案-

redis+// SpringCache下一个项目再使用

MQ+Canal-同步数据库

xxl-job预热缓存//

cookie 加密登录逻辑

调优方案

jvm-

查看堆-gc时间-查看高并发下运行时间

ps:

01案例-只需要搭建起查看时间,不需要进行各类jvm调优

1.环境搭建

太久没搭建环境了

创建的maven项目-导入的各类依赖

1.创建数据库

三个字段

主键 id (雪花算法) 用户名String --去重 密码String

create table user(id BigInt Primary key,account varchar(255) not null, password varchar(255) not null); Query OK, 0 rows affected (0.03 sec)
2.配置环境

1.配置maven 导入spring起步依赖 导入redis-导入knif34i 导入工具类

2.编写启动类-编写knif4i配置

3.导入mybutsplus-代码生成

4.自动生成mvc三层架构模式

5.插入数据库数据

insert into user(id,account,password) values(1,2,3),(2,3,4);

6.修改用户账号索引

alter table user add constraint account_unique Unique(account);
3.登录逻辑
1.controller
@RestController@RequestMapping("/api")publicclassExampleController{@AutowiredIUserServiceiUserService;@GetMapping("/login")@ApiOperation(value="登录")publicRhello(@ApiParam(value="用户ID",required=true)Stringaccount,@ApiParam(value="用户密码",required=true)Stringpassword,HttpServletRequestreq){Rr=iUserService.login(account,password);if(Objects.nonNull(r.getData())){//登录成功了的SessionwebUserDtodata=(SessionwebUserDto)r.getData();HttpSessionsession=req.getSession();session.setAttribute("session_account",data);}returnr;}}
2.service
@ServicepublicclassUserServiceImplextendsServiceImpl<UserMapper,User>implementsIUserService{@AutowiredRedisTemplateredisTemplate;@OverridepublicRlogin(Stringaccount,Stringpassword){//查询redis缓存Objecto=redisTemplate.opsForValue().get(account);if(o==null){//弹出验证码-或者其他策略 查mysql//mysql也为空LambdaQueryWrapper<User>lambdaQueryWrapper=newLambdaQueryWrapper<>();lambdaQueryWrapper.eq(User::getAccount,account);// 使用 Lambda 表达式引用字段 lambdaQueryWrapper.ge(User::getAge, 18);Useruser=this.baseMapper.selectOne(lambdaQueryWrapper);o=user;if(user==null){returnR.error("账号或密码错误");}//插入user缓存到redisredisTemplate.opsForValue().set(account,user);}//账号对了Useruser=(User)o;if(!user.getPassword().equals(password)){returnR.error("账号或密码错误");}//登录成功给sessioSessionwebUserDtosessionwebUserDto=newSessionwebUserDto();sessionwebUserDto.setAccountId(user.getAccount());returnR.success(sessionwebUserDto);}}

demo 登录注册抢单-核销lua-redis原子性

主要是的redis进行操作-看看redis的延迟任务能不能加入进来

加密token编写

对明文seiion转json后加密

非对称和对称

加密解密都在服务器-选择对称加密

采用base64编码,

需要手动解析data-赋值给对象

@Configuration@DatapublicclassTokenEncryption{@Value("${token.encryption.key}")privateStringkey;@Value("${token.encryption.iv}")privateStringiv;@Value("${token.encryption.algorithm}")privateStringalgorithm;privateSecretKeySpecAES;privateIvParameterSpecivSpec;privateCiphercipher;publicStringgetKey(){returnkey;}publicStringgetIv(){returniv;}publicStringgetAlgorithm(){returnalgorithm;}publicStringgetTokenEncryption(Stringdata)throwsException{//用于包装密钥 --包装向量extracted();//指定加密//加密cipher.init(Cipher.ENCRYPT_MODE,AES,ivSpec);byte[]encrypted=cipher.doFinal(data.getBytes());StringencryptedBase64=Base64.getEncoder().encodeToString(encrypted);returnencryptedBase64;}privatevoidextracted()throwsException{if(AES==null){byte[]rawKey=adjustKeyLength(key,16);// 16字节=128位AES=newSecretKeySpec(rawKey,algorithm);}if(ivSpec==null){byte[]ivBytes=adjustKeyLength(iv,16);ivSpec=newIvParameterSpec(ivBytes);}if(cipher==null){cipher=Cipher.getInstance("AES/CBC/PKCS5Padding");}}privateSecretKeySpecsecretKey;publicStringretToken(Stringdata)throwsException{if(secretKey==null){byte[]rawKey=adjustKeyLength(key,16);// 16字节=128位secretKey=newSecretKeySpec(rawKey,algorithm);}if(ivSpec==null){byte[]ivBytes=adjustKeyLength(iv,16);ivSpec=newIvParameterSpec(ivBytes);}// 指定解密算法和模式Ciphercipher=Cipher.getInstance("AES/CBC/PKCS5Padding");// 初始化解密器cipher.init(Cipher.DECRYPT_MODE,secretKey,ivSpec);// 解密Base64编码的加密数据byte[]decodedEncryptedData=Base64.getDecoder().decode(data);byte[]decryptedData=cipher.doFinal(decodedEncryptedData);// 返回解密后的原始字符串returnnewString(decryptedData,"UTF-8");}privatebyte[]adjustKeyLength(Stringkey,intlength)throwsException{byte[]keyBytes=key.getBytes("UTF-8");if(keyBytes.length==length){returnkeyBytes;}elseif(keyBytes.length<length){// 如果长度不足,使用0填充byte[]paddedKey=newbyte[length];System.arraycopy(keyBytes,0,paddedKey,0,keyBytes.length);returnpaddedKey;}else{// 如果长度超出,截取前面的部分byte[]shortenedKey=newbyte[length];System.arraycopy(keyBytes,0,shortenedKey,0,length);returnshortenedKey;}}}

脑子一抽就写出来了,本来准备套JWT的。。

我脑子抽了-

然后-关于过期就加入时间搓效验-我没有加-后续能人再加把-嘻嘻

拦截器编写
@Configuration public class WebConfig implements WebMvcConfigurer { public void addInterceptors(InterceptorRegistry registry){ registry.addInterceptor(new MyInterceptor()) .addPathPatterns("/**") // 拦截所有请求 .excludePathPatterns("/api/login", "/error"); // 排除某些路径不拦截 } }
@Component public class MyInterceptor implements HandlerInterceptor { @Autowired TokenEncryption tokenEncryption; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { /* HttpSession session = request.getSession(); String token= (String) session.getAttribute("session_account");*/ if(tokenEncryption==null) { tokenEncryption=new TokenEncryption(); /* key: "w7HqL+Jz3Kt0J3u6fYT3Ow==" iv: "77b07a672d57d64c" algorithm: AES */ tokenEncryption.setKey("w7HqL+Jz3Kt0J3u6fYT3Ow=="); tokenEncryption.setIv("77b07a672d57d64c"); tokenEncryption.setAlgorithm("AES"); } Cookie[] cookies = request.getCookies(); if (cookies == null) { throw new BusinessException("用户错误"); } for (Cookie cookie : cookies) { if ("username".equals(cookie.getName())) { String username = cookie.getValue(); String userid =tokenEncryption.retToken(username); return true; } } throw new BusinessException("用户错误"); } }
全局异常编写

我redis忘记启动了,报错信息直接给我出了-redis地址-笑死我了

@RestControllerAdvice public class ExceptionHandler { @org.springframework.web.bind.annotation.ExceptionHandler(value = Exception.class) public R handleException(Exception e) { e.getMessage(); return R.error("对不起,操作失败,请联系管理员"); } @org.springframework.web.bind.annotation.ExceptionHandler(value = BusinessException.class) public R handleException(BusinessException e) { return R.error( e.getMessage()); } }
public class BusinessException extends RuntimeException { private ResponseCodeEnum codeEnum; private Integer code; private String message; public BusinessException(String message, Throwable e) { super(message, e); this.message = message; } public BusinessException(String message) { super(message); this.message = message; } public BusinessException(Throwable e) { super(e); } public BusinessException(ResponseCodeEnum codeEnum) { super(codeEnum.getMsg()); this.codeEnum = codeEnum; this.code = codeEnum.getCode(); this.message = codeEnum.getMsg(); } public BusinessException(Integer code, String message) { super(message); this.code = code; this.message = message; } public ResponseCodeEnum getCodeEnum() { return codeEnum; } public Integer getCode() { return code; } @Override public String getMessage() { return message; } /** * 重写fillInStackTrace 业务异常不需要堆栈信息,提高效率. */ @Override public Throwable fillInStackTrace() { return this; } }

10/4号代码

注册编写

以后碰到异常我自己捕获了,不能老是抛抛抛

使用IdType.ASSIGN_ID(雪花算法)

MyBatis-Plus 的内置雪花算法来生成一个Long类型的唯一 ID

@TableId(value = "id", type = IdType.ASSIGN_UUID) private Long id;
@Override public R register(User user) { if(user==null||user.getAccount() == null||user.getPassword()==null) { return R.error("请补全参数"); } //各类校验 Object o = redisTemplate.opsForValue().get(user.getAccount()); if(o!=null) { return R.error("账号已存在"); } ///后续做xxl-job /* String keyRegister = "register"+user.getAccount(); redisTemplate.opsForValue().set(keyRegister,user.getPassword());*/ int insert = this.baseMapper.insert(user); if(insert==0) { return R.error("注册失败"); } //即时缓存 redisTemplate.opsForValue().set(user.getAccount(),user.getPassword()); return R.success("注册成功"); }
设计出错

redis设计结构出错-应该是id-配合整个类()

不然拿不到id-主键搜索本身就是聚集索引快-而账户搜索就不是

前端编写
抢卷发布0
-redis各类技术

设计思路-

我发布抢卷mysql

mysql-canlce—redis进行缓存–解决缓存不一致

redis各类击穿问题

reids-spring cache

redis原子性操作

抢卷发布
数据库设计

抢卷ID -抢卷库存

抢卷表(Coupon)

抢卷记录表(Coupon_Redemption)

createtablecoupon(idBIGINTAUTO_INCREMENTPRIMARYKEY,coupon_codevarchar(255)notnullUNIQUE,total_stockint,available_stockintnotnull,start_timedatetimenotnull,end_timedatetimenotnull,statusTINYINTNOTNULLDEFAULT0COMMENT'0: 未开始, 1: 进行中, 2: 已结束',indexidex_coupon_status(status));Query OK,0rowsaffected(0.03sec)
createtableCoupon_Redemption(idbigintAUTO_INCREMENTprimarykey,user_idVarchar(255),coupon_idbigintnotnull,redemption_timeDATETIMEnotnull,statusTINYINTNOTNULLDEFAULT1COMMENT'0: 失败, 1: 成功',foreignkey(coupon_id)referencescoupon(id)ONDELETECASCADE,uniquekeyuk_user_coupon(user_id,coupon_id));
发卷思路

reids抢卷结构

1.redis抢卷表记录

redisTemplate.opsForValue().set(coupon.getId(), coupon);

id+整个表-

再次设计问题

redis+加上前缀-例如-登录+账户-优惠卷+id

发卷service
@RestController @RequestMapping("/coupon") public class CouponController { @Autowired ICouponService iCouponService; @PostMapping("/publish") @ApiOperation(value = "发卷") public R publish(@RequestBody Coupon coupon) { return iCouponService.publish(coupon); } }
@Service public class CouponServiceImpl extends ServiceImpl<CouponMapper, Coupon> implements ICouponService { @Autowired RedisTemplate redisTemplate; @Override public R publish(Coupon coupon) { //判断--太多硬编码了-懒得用常量-谁喜欢改谁改 coupon= this.ifCoupon(coupon); //xxl-job每日扫描-防止错误 boolean i=save(coupon); if(!i) { return R.error("发布失败"); } redisTemplate.opsForValue().set("coupon"+coupon.getId().toString(), coupon); return R.success(coupon); } public Coupon ifCoupon(Coupon coupon) { if(coupon.getTotalStock()==null) { coupon.setTotalStock(10); } if(coupon.getAvailableStock()==null) { coupon.setAvailableStock(coupon.getTotalStock()); } if(coupon.getStartTime()==null) { coupon.setStartTime(LocalDateTime.now()); } if(coupon.getEndTime()==null) { coupon.setEndTime(LocalDateTime.now().plusDays(1));//默认一天后结束 } if(coupon.getCouponCode()==null) { coupon.generateCouponCode(); } if(coupon.getStatus()==null) { coupon.setStatus(0); } return coupon; } }
xxl-job 定时发布优惠卷

步骤 1.搭建调度中心-创建调度器-创建任务

配置调度中心-添加任务执行调度代码

@Component public class issueJob { @Autowired ICouponService iCouponService; /* 创建一个与 issueJob 类关联的日志记录器。 日志记录器用于输出日志信息,以帮助开发者在运行时了解程序的执行状态或排查问题。 */ @XxlJob("fajuan") public void testJob() { Coupon coupon=new Coupon(); ///coupon iCouponService.publish(coupon); } }
抢卷逻辑

////判断请求参数

@Service public class CouponRedemptionServiceImpl extends ServiceImpl<CouponRedemptionMapper, CouponRedemption> implements ICouponRedemptionService { @Autowired RedisTemplate redisTemplate; @Override public R qiangjuan(CouponRedemption coupon) { String userId = (String) RequestContextHolder.getRequestAttributes().getAttribute("userId", RequestAttributes.SCOPE_REQUEST); if (coupon.getUserId() == null) { return R.error("用户id不能为空"); } if (coupon.getCouponId() == null) { return R.error("活动查询失败"); } //查询活动是否开始 ValueOperations valueOperations = redisTemplate.opsForValue(); Object o = valueOperations.get("coupon" + coupon.getCouponId()); Coupon coupon2 = new Coupon(); coupon2 = (Coupon) o; if (coupon2.getStatus() == 0) { return R.error("活动未开始"); } LocalDateTime now = LocalDateTime.now(); if (now.isBefore(coupon2.getStartTime())) { System.out.println("活动尚未开始"); } else if (now.isAfter(coupon2.getEndTime())) { System.out.println("活动已结束"); } if (coupon2.getTotalStock() <= 0) { return R.error("库存不足"); } //开始抢卷 return null; } }

///执行抢卷脚本

eval执行lua脚本-redistempplate替换 MULTL开始redis的事务

lua脚本 --抢卷 --队列-抢卷成功队列 -设计库存-修改库存表的可用库存 --优惠卷是否抢过 local couponNum=redis.call("HGET",KEYS[3],ARGV[2]) -- hget 获取不到数据返回false而不是nil if couponNum ~= false and tonumber(couponNum) >= 1 then return "-1"; end --库存是否充足 local stockNum=redis.call("HGET",KEYS[1],ARGV[1]) if stockNum ~= false or tonumber(stockNum) < 1 then return "-2"; end --减库存 --写入抢卷成功队列

///返回抢卷结果

-1: 限领一张

-2: 已抢光

-3: 写入抢券成功队列失败,返回给用户为:抢券失败

-4: 已抢光

-5: 写入抢券同步队列失败,返回给用户为:抢券失败


///线程池xxl-job进行抢卷结果同步

崩溃一天

做redis-给redis加了个序列化器-导致后续代码一直获取redis失败

@Service public class CouponRedemptionServiceImpl extends ServiceImpl<CouponRedemptionMapper, CouponRedemption> implements ICouponRedemptionService { @Autowired RedisTemplate redisTemplate; ///@Resource(name = "Lua_test01") /// DefaultRedisScript<Integer> seizeCouponScript; @Override public R qiangjuan(CouponRedemption coupon) { String userId = (String) RequestContextHolder.getRequestAttributes().getAttribute("userId", RequestAttributes.SCOPE_REQUEST); if (userId == null) { return R.error("用户id不能为空"); } coupon.setUserId(userId); if (coupon.getCouponId() == null) { return R.error("活动查询失败"); } //查询活动是否开始 // redisTemplate.opsForValue().set("coupon"+coupon.getCouponCode().toString(), coupon); Object o= redisTemplate.opsForValue().get("coupon"+coupon.getCouponId().toString()); if(o==null) { return R.error("活动查询失败"); } Coupon coupon2 = new Coupon(); coupon2 = (Coupon) o; if (coupon2.getStatus() == 0) { return R.error("活动未开始"); } LocalDateTime now = LocalDateTime.now(); if (now.isBefore(coupon2.getStartTime())) { System.out.println("活动尚未开始"); } else if (now.isAfter(coupon2.getEndTime())) { System.out.println("活动已结束"); } if (coupon2.getTotalStock() <= 0) { return R.error("库存不足"); } // 同步队列redisKey // 资源库存redisKey int index = (int) (UUID.randomUUID().hashCode() % 10); String resourceStockRedisKey = String.format("coupon%s", index); String couponSeizeSyncRedisKey = String.format("COUPON_SEIZE_SYNC_QUEUE_NAME%s", index); // 抢券成功列表 String couponSeizeListRedisKey = String.format("COUPON_SEIZE%s%s",coupon.getCouponId(), index); /* List<String> list=new ArrayList<>(); list.add(couponSeizeSyncRedisKey);// 同步队列redisKey list.add(resourceStockRedisKey); // 资源库存redisKey list.add(couponSeizeListRedisKey); // 抢券成功列表 用户id 优惠卷id */ List<String> list=new ArrayList<>(); list.add(couponSeizeSyncRedisKey);// 同步队列redisKey list.add(resourceStockRedisKey); // 资源库存redisKey list.add(couponSeizeListRedisKey); // 抢券成功列表 //开始抢卷 // Object execute=redisTemplate.execute(seizeCouponScript,list,userId,coupon.getCouponId()); return null; } }
优惠卷库存同步

ps:以前跟着别人写简简单单,自己真的就老实了-逆天时间复杂度

@Configuration public class kcyure { @Autowired RedisTemplate redisTemplate; @XxlJob(value = "kcyure") public void kcyure() { try { //Set<String> couponKeys = redisTemplate.keys("*"); // 假设你的优惠券键以 "coupon:" 开头 //拿出同步库存表的-进行id过滤掉已经同步过的库存 //拿去同步表ids Cursor<byte[]> couponKeys = redisTemplate.getConnectionFactory().getConnection().scan(ScanOptions.scanOptions().match("*coupon*").count(100).build()); Set<String> couponKeyss = new HashSet<>(); while (couponKeys.hasNext()) { byte[] key = couponKeys.next(); String keyStr = new String(key, StandardCharsets.UTF_8); // 转换为字符串 // 提取 coupon 后面的部分 if (keyStr.contains("coupon")) { String suffix = keyStr.substring(keyStr.indexOf("coupon") + "coupon".length()); couponKeyss.add(suffix); } } //拿去库存表 Set<String> stocks = new HashSet<>(); Cursor<byte[]> stocksi = redisTemplate.getConnectionFactory().getConnection() .scan(ScanOptions.scanOptions().match("stock*").count(100).build()); while (stocksi.hasNext()) { byte[] key = stocksi.next(); String keyStr = new String(key, StandardCharsets.UTF_8); // 转换为字符串 Map<Object, Object> hashEntries = redisTemplate.opsForHash().entries(keyStr); for(Object key1 : hashEntries.keySet()) { stocks.add(keyStr); } } if (couponKeyss != null) { for (String key : couponKeyss) { if(stocks.contains(key)) { continue; } Coupon coupon= (Coupon) redisTemplate.opsForValue().get("coupon"+key.toString()); int index = (int) (coupon.getId() % 10); String resourceStockRedisKey = String.format("Stock:%s", index); redisTemplate.opsForHash().put(resourceStockRedisKey, key, coupon.getAvailableStock()); } } }catch (Exception e) { e.printStackTrace(); System.out.println("库存同步失败"); //异常处理 }}}
设计问题

优惠卷id-我设置的String-用的uuid-一直有— 我设置了LOng的-后续抢卷脚本转long才能找到数据-设计序列化-哈希计算的问题

redis序列化-更换日期类型-设置日期格式

10/5 内容

抢卷脚本
--抢卷 --队列-抢卷成功队列 -设计库存-修改库存表的可用库存 ---- key: 抢券同步队列,资源库存,抢券成功列表 -- argv:活动id,用户id --优惠卷是否抢过--抢卷成功队列--key coupon local couponNum = redis.call("HGET", KEYS[3], ARGV[2]) -- hget 获取不到数据返回false而不是nil if couponNum ~= false and tonumber(couponNum) >= 1 then return "-1"; end --库存是否充足 local stockNum=redis.call("HGET",KEYS[2],ARGV[1]) if stockNum == false or tonumber(stockNum) < 1 then return "-2"; end --抢券列表 local listNum = redis.call("HSET",KEYS[3], ARGV[2], 1) if listNum == false or tonumber(listNum) < 1 then return "-3"; end --减库存 stockNum=redis.call("HINCRBY",KEYS[2],ARGV[1],-1) if tonumber(stockNum)<0 then return "-4"; end --写入抢卷成功队列 local result=redis.call("HSETNX",KEYS[1],ARGV[2],ARGV[1]) if result>0 then return ARGV[1].."" end return "-5"
抢卷开发
@Configuration public class kcyure { @Autowired RedisTemplate redisTemplate; @XxlJob(value = "kcyure") public void kcyure() { try { //Set<String> couponKeys = redisTemplate.keys("*"); // 假设你的优惠券键以 "coupon:" 开头 //拿出同步库存表的-进行id过滤掉已经同步过的库存 //拿去同步表ids Cursor<byte[]> couponKeys = redisTemplate.getConnectionFactory().getConnection().scan(ScanOptions.scanOptions().match("*coupon*").count(100).build()); Set<String> couponKeyss = new HashSet<>(); while (couponKeys.hasNext()) { byte[] key = couponKeys.next(); String keyStr = new String(key, StandardCharsets.UTF_8); // 转换为字符串 // 提取 coupon 后面的部分 if (keyStr.contains("coupon")) { String suffix = keyStr.substring(keyStr.indexOf("coupon") + "coupon".length()); couponKeyss.add(suffix); } } //拿去库存表 Set<String> stocks = new HashSet<>(); Cursor<byte[]> stocksi = redisTemplate.getConnectionFactory().getConnection() .scan(ScanOptions.scanOptions().match("stock*").count(100).build()); while (stocksi.hasNext()) { byte[] key = stocksi.next(); String keyStr = new String(key, StandardCharsets.UTF_8); // 转换为字符串 Map<Object, Object> hashEntries = redisTemplate.opsForHash().entries(keyStr); for(Object key1 : hashEntries.keySet()) { stocks.add(keyStr); } } if (couponKeyss != null) { for (String key : couponKeyss) { if(stocks.contains(key)) { continue; } Coupon coupon= (Coupon) redisTemplate.opsForValue().get("coupon"+key.toString()); int index = (int) (coupon.getId() % 10); String resourceStockRedisKey = String.format("Stock:%s", index); redisTemplate.opsForHash().put(resourceStockRedisKey, key, coupon.getAvailableStock()); } } }catch (Exception e) { e.printStackTrace(); System.out.println("库存同步失败"); //异常处理 }}}
库存同步

线程池-配合xxl-job

@Configuration public class syncThreadPool { @Bean("syncThreadPool") public ThreadPoolExecutor syncThreadPool(){ int corePoolSize = 1; // 核心线程数 int maxPoolSize = 10; // 最大线程数 long keepAliveTime = 120; // 线程空闲时间 TimeUnit unit = TimeUnit.SECONDS; // 时间单位 // 指定拒绝策略为 DiscardPolicy RejectedExecutionHandler rejectedHandler = new ThreadPoolExecutor.DiscardPolicy(); // 任务队列,使用SynchronousQueue容量为1,在没有线程去消费时不会保存任务 ThreadPoolExecutor executor = new ThreadPoolExecutor (corePoolSize, maxPoolSize, keepAliveTime, unit, new SynchronousQueue<>(),rejectedHandler); return executor; } }

xxl-job

public class getData implements Runnable { private int index; public getData(int index){ this.index = index; } @Override public void run() { //读取任务-加锁-免得重复读取 //创建示例 RedissonClient redissonClient = Redisson.create(); //获取锁 RLock loc=redissonClient.getLock("ying"); try { boolean isLock=loc.tryLock(3, -1, TimeUnit.SECONDS); if (isLock) { //锁内执行 最大等待时间 持锁时间 分钟数 //-2看门狗机制-只要执行完成后才会放锁 String quene=String.format("ying:%s",index); jgtb(quene); } }catch (Exception e) { // 处理中断异常 }finally { if(lock!=null &&) } } public void jgtb(String quene) { } }
库存同步代码
@XxlJob(value = "kctb") public void qjtb_syncThreadPool() { for (int i=0;i<10;i++) { threadPool.execute(getData); } }
@ComponentpublicclassgetDataimplementsRunnable{@AutowiredprivateRedisTemplateredisTemplate;@AutowiredprivateICouponRedemptionServiceiCouponRedemptionService;@Overridepublicvoidrun(){//读取任务-加锁-免得重复读取//创建示例RedissonClientredissonClient=Redisson.create();//获取锁RLockloc=redissonClient.getLock("ying");try{booleanisLock=loc.tryLock(3,-1,TimeUnit.SECONDS);if(isLock){//锁内执行 最大等待时间 持锁时间 分钟数//-2看门狗机制-只要执行完成后才会放锁intindex=(int)(Math.random()*10)+1;Stringquene=String.format("ying:%s",index);jgtb(quene);}}catch(Exceptione){// 处理中断异常}finally{if(loc!=null&&loc.isLocked()){loc.unlock();}}}publicvoidjgtb(Stringquene){/// Cursor<byte[]> couponKeys = redisTemplate.getConnectionFactory()// .getConnection().scan(// ScanOptions.scanOptions().match("*coupon*").count(100).build());//获取成功数据 -游标拿去try{List<String>stocks=newArrayList<>();Cursor<byte[]>stocksi=redisTemplate.getConnectionFactory().getConnection().scan(ScanOptions.scanOptions().match(quene).count(100).build());if(stocksi==null){return;}while(stocksi.hasNext()){byte[]key=stocksi.next();StringkeyStr=newString(key,StandardCharsets.UTF_8);// 转换为字符串-队列名字Map<String,Object>hashEntries=redisTemplate.opsForHash().entries(keyStr);//根据队列名字找key集合for(Stringkey1:hashEntries.keySet()){stocks.add(key1);}}if(stocks.size()==0){return;}for(Stringkey:stocks){CouponRedemptioncouponRedemption=newCouponRedemption();couponRedemption.setUserId(String.valueOf(key));//真是神仙代码Longo=(Long)redisTemplate.opsForHash().get(quene,key);couponRedemption.setCouponId(String.valueOf(o));couponRedemption.setRedemptionTime(newDate());couponRedemption.setStatus(1);iCouponRedemptionService.save(couponRedemption);redisTemplate.opsForHash().delete(quene,key);}}catch(Exceptione){e.printStackTrace();}}}
mysql外键错误

商品卷约束到父表user去了-笑死我了

alter table coupon_redemption add constraint coupon_redemption_coupon_coupon_code_fk foreign key (coupon_id) references coupon (coupon_code);
优惠卷展示
@Override public R lsit() { //这里偷懒不用游标了 //权限校验 Set<String> keys = redisTemplate.keys("coupon*"); List<Object> values = new ArrayList<>(); for (String key : keys) { values.add(redisTemplate.opsForValue().get(key)); } return R.success(values); }
拦截器修复拦截knife4i
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { /* HttpSession session = request.getSession(); String token= (String) session.getAttribute("session_account");*/ HandlerMethod handlerMethod=(HandlerMethod)handler; //判断如果请求的类是swagger的控制器,直接通行。 if(handlerMethod.getBean().getClass().getName().equals("springfox.documentation.swagger.web.ApiResourceController")){ return true; }
总结
项目地址

https://gitee.com/laomaodu/anli01

api地址

https://apifox.com/apidoc/shared-20ef25e6-5dfe-482f-88a0-ae48011048a3

心得

对于代码逻辑来讲简单-但是总会涉及到很多bug-很多的bug都是再设计时候埋下的坑,下一次项目就会注重设计了

https://www.bilibili.com/video/BV14N1qYNEbU/?spm_id_from=333.999.0.0

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/25 19:45:02

当AI成为你的“学术副导师”:Paperzz如何用3000字重构毕业论文写作的底层逻辑——从选题焦虑到一键生成,一个不靠“灌水”的智能协作方案

Paperzz-AI官网免费论文查重复率AIGC检测/开题报告/文献综述/论文初稿 paperzz - 毕业论文-AIGC论文检测-AI智能降重-ai智能写作https://www.paperzz.cc/dissertation 引言&#xff1a;我们不是在写论文&#xff0c;就是在为论文崩溃的路上 你有没有经历过这样的夜晚&#xf…

作者头像 李华
网站建设 2026/4/2 21:05:38

全连接神经网络深度解析:从入门到实战应用

全连接神经网络深度解析&#xff1a;从入门到实战应用 【免费下载链接】全连接神经网络多层感知机PPT详细介绍 这份PPT资源是学习全连接神经网络&#xff08;多层感知机&#xff0c;MLP&#xff09;的绝佳指南&#xff0c;内容全面且易于理解。它从单层感知机的基础概念入手&am…

作者头像 李华
网站建设 2026/3/29 21:15:17

毕业设计 基于机器视觉的人体姿态行为识别

文章目录 0 简介**1、人体姿态估计简介**2、人体姿态估计数据集4、实现原理5、实现神经网络6 部分关键代码7 最后 0 简介 今天学长向大家分享一个毕业设计项目 毕业设计 基于机器视觉的人体姿态行为识别 项目运行效果&#xff1a; 毕业设计 深度学习人体姿势姿态识别&#x…

作者头像 李华
网站建设 2026/3/26 6:57:07

FLUX.1 Kontext:从零开始掌握120亿参数AI图像编辑神器

FLUX.1 Kontext&#xff1a;从零开始掌握120亿参数AI图像编辑神器 【免费下载链接】FLUX.1-Kontext-dev 项目地址: https://ai.gitcode.com/hf_mirrors/black-forest-labs/FLUX.1-Kontext-dev AI图像编辑技术正经历革命性变革&#xff0c;从简单的图像生成走向精准的智…

作者头像 李华
网站建设 2026/4/3 4:43:12

爱普生L4158系列打印机清零终极教程:永久版软件操作指南

爱普生L4158系列打印机清零终极教程&#xff1a;永久版软件操作指南 【免费下载链接】爱普生L4150L4160L4151L4153L4156L4158L4163L4166L4167L4168L4169清零软件图解 本仓库提供爱普生L4150、L4160、L4151、L4153、L4156、L4158、L4163、L4166、L4167、L4168、L4169系列打印机的…

作者头像 李华