优惠卷发布平台

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索引优化

01案例,不想想太多,简简单单上个手

设计思路-

数据库 id 账号 密码

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

技术方案-

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

MQ+Canal-同步数据库

xxl-job预热缓存//

cookie 加密登录逻辑

调优方案

jvm-

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

ps:

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

1.环境搭建

太久没搭建环境了

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

1.创建数据库

三个字段

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

1
2
 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.插入数据库数据

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

6.修改用户账号索引

1
alter table user add constraint account_unique Unique(account);

3.登录逻辑

1.controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

@RestController
@RequestMapping("/api")
public class ExampleController {
@Autowired
IUserService iUserService;
@GetMapping("/login")
@ApiOperation(value = "登录")

public R hello(@ApiParam(value = "用户ID", required = true)String account, @ApiParam(value = "用户密码", required = true) String password , HttpServletRequest req) {
R r=iUserService.login(account,password);
if(Objects.nonNull(r.getData()))
{
//登录成功了的
SessionwebUserDto data = (SessionwebUserDto) r.getData();
HttpSession session = req.getSession();
session.setAttribute("session_account",data);
}

return r;
}
}

2.service

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
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Autowired
RedisTemplate redisTemplate;

@Override
public R login(String account, String password) {
//查询redis缓存
Object o = redisTemplate.opsForValue().get(account);
if(o==null)
{
//弹出验证码-或者其他策略 查mysql
//mysql也为空
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(User::getAccount, account); // 使用 Lambda 表达式引用字段 lambdaQueryWrapper.ge(User::getAge, 18);
User user = this.baseMapper.selectOne(lambdaQueryWrapper);
o=user;
if(user==null)
{
return R.error("账号或密码错误");
}
//插入user缓存到redis
redisTemplate.opsForValue().set(account,user);

}
//账号对了
User user = (User) o;
if(!user.getPassword().equals(password))
{
return R.error("账号或密码错误");
}
//登录成功给sessio
SessionwebUserDto sessionwebUserDto = new SessionwebUserDto();
sessionwebUserDto.setAccountId(user.getAccount());

return R.success(sessionwebUserDto);

}
}

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

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

加密token编写

对明文seiion转json后加密

非对称和对称

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

采用base64编码,

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

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112

@Configuration
@Data
public class TokenEncryption {
@Value("${token.encryption.key}")
private String key;

@Value("${token.encryption.iv}")
private String iv;

@Value("${token.encryption.algorithm}")
private String algorithm;


private SecretKeySpec AES;

private IvParameterSpec ivSpec;
private Cipher cipher;

public String getKey() {
return key;
}

public String getIv() {
return iv;
}

public String getAlgorithm() {
return algorithm;
}

public String getTokenEncryption(String data) throws Exception {
//用于包装密钥 --包装向量
extracted();
//指定加密
//加密
cipher.init(Cipher.ENCRYPT_MODE, AES, ivSpec);
byte[] encrypted = cipher.doFinal(data.getBytes());
String encryptedBase64 = Base64.getEncoder().encodeToString(encrypted);


return encryptedBase64;

}

private void extracted() throws Exception {
if(AES==null)
{
byte[] rawKey = adjustKeyLength(key, 16); // 16字节=128位
AES=new SecretKeySpec(rawKey,algorithm);
}
if(ivSpec==null)
{
byte[] ivBytes = adjustKeyLength(iv, 16);
ivSpec = new IvParameterSpec(ivBytes);
}
if(cipher == null)
{
cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
}
}

private SecretKeySpec secretKey;

public String retToken(String data) throws Exception {
if(secretKey==null)
{
byte[] rawKey = adjustKeyLength(key, 16); // 16字节=128位
secretKey = new SecretKeySpec(rawKey, algorithm);
}

if(ivSpec==null)
{
byte[] ivBytes = adjustKeyLength(iv, 16);
ivSpec = new IvParameterSpec(ivBytes);
}
// 指定解密算法和模式
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");

// 初始化解密器
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec);

// 解密Base64编码的加密数据
byte[] decodedEncryptedData = Base64.getDecoder().decode(data);
byte[] decryptedData = cipher.doFinal(decodedEncryptedData);

// 返回解密后的原始字符串
return new String(decryptedData, "UTF-8");


}

private byte[] adjustKeyLength(String key, int length) throws Exception {
byte[] keyBytes = key.getBytes("UTF-8");
if (keyBytes.length == length) {
return keyBytes;
} else if (keyBytes.length < length) {
// 如果长度不足,使用0填充
byte[] paddedKey = new byte[length];
System.arraycopy(keyBytes, 0, paddedKey, 0, keyBytes.length);
return paddedKey;
} else {
// 如果长度超出,截取前面的部分
byte[] shortenedKey = new byte[length];
System.arraycopy(keyBytes, 0, shortenedKey, 0, length);
return shortenedKey;
}
}


}

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

我脑子抽了-

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

拦截器编写

1
2
3
4
5
6
7
8
9
10
11
@Configuration

public class WebConfig implements WebMvcConfigurer {

public void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(new MyInterceptor())
.addPathPatterns("/**") // 拦截所有请求
.excludePathPatterns("/api/login", "/error"); // 排除某些路径不拦截

}
}
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
@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地址-笑死我了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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());
}
}
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
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

1
2
@TableId(value = "id", type = IdType.ASSIGN_UUID)
private Long 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
@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)

c8e2f1f67aa728ad3b18a0916b36c25b

抢卷记录表(Coupon_Redemption)

image-20241005111703478

1
2
3
4
5
6
7
create table coupon( id BIGINT AUTO_INCREMENT PRIMARY KEY,
coupon_code varchar(255) not null UNIQUE
,total_stock int,available_stock int not null
,start_time datetime not null,end_time datetime not null
,status TINYINT NOT NULL DEFAULT 0 COMMENT '0: 未开始, 1: 进行中, 2: 已结束',
index idex_coupon_status(status));
Query OK, 0 rows affected (0.03 sec)
1
2
3
4
5
6
7
8
9
10
create table Coupon_Redemption (
id bigint AUTO_INCREMENT primary key,
user_id Varchar(255),
coupon_id bigint not null,
redemption_time DATETIME not null,
status TINYINT NOT NULL DEFAULT 1 COMMENT '0: 失败, 1: 成功',
foreign key(coupon_id) references coupon(id) ON DELETE CASCADE,
unique key uk_user_coupon (user_id, coupon_id)

);

发卷思路

f360eea2a393757063c2caf95fe55ecc

reids抢卷结构

1.redis抢卷表记录

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

id+整个表-

再次设计问题

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

发卷service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@RequestMapping("/coupon")
public class CouponController {
@Autowired
ICouponService iCouponService;

@PostMapping("/publish")
@ApiOperation(value = "发卷")
public R publish(@RequestBody Coupon coupon) {
return iCouponService.publish(coupon);
}

}

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
@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.搭建调度中心-创建调度器-创建任务

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Component
public class issueJob {


@Autowired
ICouponService iCouponService;
/*
创建一个与 issueJob 类关联的日志记录器。
日志记录器用于输出日志信息,以帮助开发者在运行时了解程序的执行状态或排查问题。
*/

@XxlJob("fajuan")
public void testJob() {
Coupon coupon=new Coupon();
///coupon
iCouponService.publish(coupon);
}
}

抢卷逻辑

////判断请求参数

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
@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的事务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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失败

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
@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:以前跟着别人写简简单单,自己真的就老实了-逆天时间复杂度

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
@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 内容

抢卷脚本

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
--抢卷
--队列-抢卷成功队列 -设计库存-修改库存表的可用库存
---- 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"


抢卷开发

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@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

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
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)
{

}
}

库存同步代码

1
2
3
4
5
6
7
8
9
@XxlJob(value = "kctb")
public void qjtb_syncThreadPool()
{
for (int i=0;i<10;i++)
{
threadPool.execute(getData);
}

}
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
@Component
public class getData implements Runnable {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private ICouponRedemptionService iCouponRedemptionService;


@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看门狗机制-只要执行完成后才会放锁
int index = (int)(Math.random() * 10) + 1;
String quene=String.format("ying:%s",index);
jgtb(quene);

}
}catch (Exception e)
{
// 处理中断异常
}finally {
if (loc != null && loc.isLocked()) {
loc.unlock();
}
}


}
public void jgtb(String quene)
{/// Cursor<byte[]> couponKeys = redisTemplate.getConnectionFactory()
// .getConnection().scan(
// ScanOptions.scanOptions().match("*coupon*").count(100).build());
//获取成功数据 -游标拿去

try {
List<String> stocks = new ArrayList<>();
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();
String keyStr = new String(key, StandardCharsets.UTF_8); // 转换为字符串-队列名字
Map<String, Object> hashEntries = redisTemplate.opsForHash().entries(keyStr);//根据队列名字找key集合
for(String key1 : hashEntries.keySet())
{

stocks.add(key1);
}
}
if(stocks.size()==0)
{
return;
}
for( String key:stocks)
{
CouponRedemption couponRedemption = new CouponRedemption();
couponRedemption.setUserId(String.valueOf(key));
//真是神仙代码
Long o= (Long) redisTemplate.opsForHash().get(quene,key);
couponRedemption.setCouponId(String.valueOf(o));
couponRedemption.setRedemptionTime(new Date());
couponRedemption.setStatus(1);
iCouponRedemptionService.save(couponRedemption);
redisTemplate.opsForHash().delete(quene,key);

}


}catch (Exception e)
{
e.printStackTrace();
}
}
}

mysql外键错误

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

1
2
3
4
5
alter table coupon_redemption
add constraint coupon_redemption_coupon_coupon_code_fk
foreign key (coupon_id) references coupon (coupon_code);


优惠卷展示

1
2
3
4
5
6
7
8
9
10
11
12
13
@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

1
2
3
4
5
6
7
8
9
10
    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


优惠卷发布平台
http://example.com/2024/10/03/case/登录案例/
作者
John Doe
发布于
2024年10月3日
许可协议