Redis 实现抢红包,是我想简单了
-
新人入群,发红包+抢红包,属于高并发业务要求,不能用mysql来做,尝试用redis实现
-
一个总的大红包,会有可能拆分成多个小红包,总金额= 分金额1+分金额2+分金额3......分金额N
-
每个人只能抢一次,需要有记录,比如100块钱,被拆分成10个红包发出去,总计有10个红包,抢一个少一个,总数显示(10/6)直到抢完,需要记录哪些人抢到了红包。
-
有可能还需要你计时,从发出全部抢完,耗时多少?
-
红包过期,没人抢红包,需在24小时内退回发红包主账户下。
-
虽说是随机红包,但是红包金额如何设置才能显得相对公平?
-
高并发下如何保证数据一致性?
......
【需求分析】
基本业务流程如下: 【技术选型】 抢红包属于高并发场景,为避免频繁IO导致的性能瓶颈,故选用redis实现。【落地实现】
Redis如何支持抢红包场景的基本操作,不包括完整的业务逻辑和异常处理。要在命令行中使用Redis实现一个简单的抢红包场景,可以通过以下步骤使用redis-cli工具来执行Redis命令。 以下是生成红包 池、发红包、抢红包和红包 记录的命令示例:
1. 生成红包池:# 使用RPUSH命令向名为"red_packet_pool"的列表中添加红包金额,此处示例为10个红包,总金额100元
127.0.0.1:6379> RPUSH red_packet_pool 10 20 30 40 50 60 70 80 90 100
2. 发红包:
# 使用LPUSH命令将红包ID推送到名为"red_packet_ids"的列表中,同时也将红包金额从"red_packet_pool"中弹出
127.0.0.1:6379> LPUSH red_packet_ids RP_1
127.0.0.1:6379> LPOP red_packet_pool
3. 抢红包:
# 使用RPOP命令从"red_packet_ids"列表中获取一个红包ID
127.0.0.1:6379> RPOP red_packet_ids
4. 红包记录:
# 使用LPUSH命令将抢到的红包金额和用户ID记录到名为"red_packet_records"的列表中
127.0.0.1:6379> LPUSH red_packet_records "User1 抢到了 10元"
这只是一个简单的演示,在真实应用中,这些命令通常会由后端应用程序执行。以下是代码实现:首先,确保你的 Spring Boot 项目中已正确配置了 Redis 连接。在application.properties或application.yml中添加Redis连接配置:
spring.redis.host=localhost
spring.redis.port=6379
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
public class RedPacketService {
private RedisTemplate<String, String> redisTemplate;
public void sendRedPacket(String redPacketId, double totalAmount, int totalPeople) {
double remainingAmount = totalAmount;
for (int i = 1; i < totalPeople; i++) {
double randomAmount = Math.random() * remainingAmount / (totalPeople - i);
redisTemplate.opsForList().leftPush(redPacketId, String.format("%.2f", randomAmount));
remainingAmount -= randomAmount;
}
redisTemplate.opsForList().leftPush(redPacketId, String.format("%.2f", remainingAmount));
}
public String grabRedPacket(String redPacketId) {
String amount = redisTemplate.opsForList().rightPop(redPacketId);
if (amount != null) {
double grabbedAmount = Double.parseDouble(amount);
String userId = "User" + System.nanoTime();
String grabInfo = userId + " 抢到了 " + String.format("%.2f", grabbedAmount) + " 元";
redisTemplate.opsForList().leftPush("grabbed:" + redPacketId, grabInfo);
return grabInfo;
} else {
return "红包已抢完";
}
}
public List<String> getRedPacketRecords(String redPacketId) {
return redisTemplate.opsForList().range("grabbed:" + redPacketId, 0, -1);
}
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
public class RedPacketController {
private RedPacketService redPacketService;
public void sendRedPacket( String redPacketId, double totalAmount, int totalPeople) {
redPacketService.sendRedPacket(redPacketId, totalAmount, totalPeople);
}
public String grabRedPacket( String redPacketId) {
return redPacketService.grabRedPacket(redPacketId);
}
public List<String> getRedPacketRecords( String redPacketId) {
return redPacketService.getRedPacketRecords(redPacketId);
}
}
最后,假设你的Spring Boot应用程序已经在主机
127.0.0.1
的端口 8080
上运行。
1、发红包操作:
-
URL:
http://114.116.85.56:8080/redpacket/send
-
参数:
-
redPacketId
:红包的唯一标识符。 -
totalAmount
:红包的总金额。 -
totalPeople
:红包的总领取人数。
-
示例请求:
http://114.116.85.56:8080/redpacket/send?redPacketId=1&totalAmount=100.0&totalPeople=10
2、抢红包操作:
-
URL:
http://114.116.85.56:8080/redpacket/grab
-
参数:
-
redPacketId
:要抢的红包的唯一标识符。
-
URL: http://114.116.85.56:8080/redpacket/grab?redPacketId=1
3、 获取红包记录操作:
-
URL:
http://114.116.85.56:8080/redpacket/records
-
参数:
-
redPacketId
:要获取记录的红包的唯一标识符。
-
http://114.116.85.56:8080/redpacket/records?redPacketId=1redPacketId:要获取记录的红包的唯一标识符。
br
【痛点问题】
在抢红包过程中,可能存在一些痛点问题,这些问题需要在系统设计和实现中仔细考虑和解决。以下是一些可能存在的痛点问题:
- 高并发问题:抢红包场景通常伴随着高并发操作,多个用户同时尝试抢夺同一个红包。这可能导致竞态条件和数据一致性问题。
- 数据一致性问题:在高并发情况下,多个用户同时修改Redis中的数据,可能导致数据一致性问题。例如,多个用户同时写入抢红包记录,可能导致数据的混乱或丢失。
- 性能问题:处理高并发抢红包请求可能对系统的性能产生挑战。需要考虑系统的扩展性和负载均衡。
- 作弊问题:用户可能尝试通过不正当手段多次抢夺同一个红包。需要考虑如何检测和防止作弊行为。
- 红包池管理:红包池的管理和维护也是一个问题,包括红包的生成、过期处理和数据清理。
- 数据安全性:红包金额的安全性也是一个关键问题。需要确保用户不能通过恶意请求或攻击来窃取或篡改红包金额。
- 用户体验:最终用户的体验也是关键因素。抢红包的过程应该是流畅的,用户不应该感到等待时间过长或遇到错误。
解决这些痛点问题需要综合考虑多个因素,包括并发控制、事务处理、分布式锁、数据模型设计、性能优化、安全性等。在设计抢红包系统时,需要仔细权衡这些因素,以确保系统的可伸缩性、稳定性和用户体验。
我们就高并发问题可能导致竞态条件和数据一致性问题给出解决方案。
方案一:分布式锁
使用分布式锁来解决高并发问题的代码示例:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
public class RedPacketService {
private StringRedisTemplate stringRedisTemplate;
public String grabRedPacket(String redPacketId, String userId) {
String redPacketKey = "red_packet:" + redPacketId;
String userKey = "user:" + userId;
try {
// 使用分布式锁
boolean isLocked = stringRedisTemplate.opsForValue().setIfAbsent(userKey, "1", 10, TimeUnit.SECONDS);
if (isLocked) {
// 获取到锁,可以继续抢红包
if (stringRedisTemplate.opsForList().size(redPacketKey) > 0) {
// 红包池还有红包,可以继续抢
String redPacket = stringRedisTemplate.opsForList().leftPop(redPacketKey);
// 记录抢红包信息
String record = userId + " 抢到了 " + redPacket + " 元";
stringRedisTemplate.opsForList().leftPush("red_packet_records:" + redPacketId, record);
// 释放用户锁
stringRedisTemplate.delete(userKey);
return record;
} else {
// 红包池已空
stringRedisTemplate.delete(userKey);
return "红包已抢光";
}
} else {
// 用户未成功获取锁,表示用户已经抢过红包
return "你已经抢过红包了";
}
} catch (InterruptedException e) {
// 处理异常
e.printStackTrace();
return "抢红包出现异常";
}
}
}
方案二:Redis事务
使用Redis的事务机制来确保操作的原子性。Redis的事务允许一组操作(一系列命令)在一个单一的、原子的事务中执行,这意味着它们要么全部成功,要么全部失败。在抢红包的情况下,你可以使用 Redis 的MULTI、EXEC和WATCH命令来创建一个事务块。
# 开始一个事务
127.0.0.1:6379> MULTI
# 监视红包池的变化
127.0.0.1:6379> WATCH red_packet_pool
# 检查红包池中是否还有红包
127.0.0.1:6379> LLEN red_packet_pool
(integer) 3
# 如果红包池中还有红包,则继续操作
127.0.0.1:6379> LPUSH red_packet_ids RP_1
127.0.0.1:6379> LPOP red_packet_pool
# 提交事务
127.0.0.1:6379> EXEC
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
然后,在Spring Boot应用中创建一个RedPacketService类,该类包含了处理抢红包操作的方法:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.stereotype.Service;
public class RedPacketService {
private RedisTemplate<String, String> redisTemplate;
public String grabRedPacket(String redPacketId, String userId) {
String redPacketKey = "red_packet:" + redPacketId;
String userKey = "user:" + userId;
SessionCallback<String> sessionCallback = operations -> {
operations.watch(redPacketKey);
String redPacket = operations.opsForList().leftPop(redPacketKey);
if (redPacket != null) {
operations.multi();
operations.opsForList().leftPush("red_packet_records:" + redPacketId, userId + " 抢到了 " + redPacket + " 元");
operations.exec();
}
operations.unwatch();
return redPacket;
};
String result = redisTemplate.execute(sessionCallback);
if (result == null) {
return "红包已抢光";
} else if (result.equals("")) {
return "你已经抢过红包了";
} else {
return result;
}
}
}
在这个示例中,我们使用SessionCallback接口来执行事务。 在sessionCallback中,我们首先调用watch方法来监视红包池的变化。 然后,我们执行一系列操作,包括弹出红包、记录抢红包信息,并使用multi和exec方法来提交事务。 最后,我们使用unwatch来取消监视。
【结尾】
感谢大家认真审阅,也欢迎大佬们批评指正。 如果您觉得对日常工作或学习有帮助,欢迎点赞,在看,转发和评论。
👇🏻 点击下方阅读原文,获取鱼皮往期编程干货。
往期推荐