Redis实现秒杀:从乐观锁到Lua脚本,再到Redisson分布式锁,一场与高并发的"搏斗"
引言
大家好,今天我们来聊聊如何用Redis实现秒杀系统。秒杀,听起来很刺激,但实现起来却让人头秃。想象一下,成千上万的用户在同一时刻点击"立即购买"按钮,服务器瞬间被淹没在请求的海洋中。如果你的系统没有做好并发控制,那结果就是------崩了。
今天,我们将用Java来实现一个秒杀系统,先从乐观锁开始,再升级到Lua脚本,最后聊聊Redisson分布式锁的解决方案。准备好了吗?让我们一起进入这场与高并发的"搏斗"!
一、秒杀系统的核心问题
1.1 高并发:服务器崩溃的导火索
秒杀系统的最大挑战就是高并发。成千上万的用户在同一时刻发起请求,服务器瞬间被淹没。如果你的系统没有做好并发控制,那结果就是------崩了。
1.2 库存超卖:程序员的噩梦
另一个大问题是库存超卖。假设你有100件商品,结果因为并发问题,卖出了101件。这时候,你不仅要面对用户的投诉,还要想办法处理多余的订单。这简直就是程序员的噩梦。
1.3 订单处理:秒杀后的"烂摊子"
最后,订单处理也是一个头疼的问题。秒杀成功后,如何快速生成订单,如何保证订单的准确性,如何处理支付和发货,这些都是需要考虑的。
1.4校验一人一单
利用 Redis Set 记录已下单用户
在 Redis 中,Set 数据结构是一个无序且不重复的集合,非常适合用来记录已下单的用户标识。当用户发起抢购请求时,先检查用户 ID 是否在记录已下单用户的 Set 集合中。若存在,则表示该用户已下过单,不允许再次下单;若不存在,将用户 ID 添加到 Set 集合中,并继续后续的下单流程。
ini
public class SeckillWithRedisson {
private static final String STOCK_KEY = "seckill:stock";
private static final String ORDER_KEY_PREFIX = "seckill:order:";
private static final String ORDERED_USER_SET = "seckill:ordered:users";
public static void main(String[] args) {
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
RedissonClient redissonClient = Redisson.create(config);
RLock lock = redissonClient.getLock("seckill:lock");
Jedis jedis = new Jedis("localhost", 6379);
String userId = "user1";// 实际应用中从请求中获取真实用户ID
try {
lock.lock();
// 检查用户是否已下单
if (jedis.sismember(ORDERED_USER_SET, userId)) {
System.out.println("该用户已下过单,不允许重复下单");
return;
}
int stock = Integer.parseInt(jedis.get(STOCK_KEY));
if (stock > 0) {
jedis.set(STOCK_KEY, String.valueOf(stock - 1));
// 将用户ID添加到已下单用户集合中
jedis.sadd(ORDERED_USER_SET, userId);
String orderId = generateOrderId();
jedis.set(ORDER_KEY_PREFIX + orderId, userId);
System.out.println("抢购成功,订单号:" + orderId);
} else {
System.out.println("抢购失败,库存不足");
}
} finally {
lock.unlock();
jedis.close();
redissonClient.shutdown();
}
}
private static String generateOrderId() {
return System.currentTimeMillis() + "";
}
}
利用 Redis Hash 记录用户订单状态
另一种方式是使用 Redis 的 Hash 结构,以用户 ID 作为 Hash 的 key,订单相关信息(如订单 ID、下单时间等)作为 field-value 对。在处理抢购请求时,通过判断 Hash 中是否存在该用户 ID 的记录来确定用户是否已下单。这种方式相较于 Set,能存储更多与订单相关的详细信息,便于后续的查询和统计。
代码示例如下:
java
public class SeckillWithRedisson {
private static final String STOCK_KEY = "seckill:stock";
private static final String ORDER_KEY_PREFIX = "seckill:order:";
private static final String USER_ORDER_HASH = "seckill:user:orders";
public static void main(String[] args) {
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
RedissonClient redissonClient = Redisson.create(config);
RLock lock = redissonClient.getLock("seckill:lock");
Jedis jedis = new Jedis("localhost", 6379);
String userId = "user1";// 实际应用中从请求中获取真实用户ID
try {
lock.lock();
// 检查用户是否已下单
try {
jedis.hget(USER_ORDER_HASH, userId);
System.out.println("该用户已下过单,不允许重复下单");
return;
} catch (JedisDataException e) {
// 如果用户不存在,不抛出异常,继续后续流程
}
int stock = Integer.parseInt(jedis.get(STOCK_KEY));
if (stock > 0) {
jedis.set(STOCK_KEY, String.valueOf(stock - 1));
String orderId = generateOrderId();
// 将订单信息记录到Hash中
jedis.hset(USER_ORDER_HASH, userId, orderId);
jedis.set(ORDER_KEY_PREFIX + orderId, userId);
System.out.println("抢购成功,订单号:" + orderId);
} else {
System.out.println("抢购失败,库存不足");
}
} finally {
lock.unlock();
jedis.close();
redissonClient.shutdown();
}
}
private static String generateOrderId() {
return System.currentTimeMillis() + "";
}
}
这两种方法各有优劣,使用 Set 结构较为简洁,内存占用少,适用于仅需判断用户是否下单的场景;而 Hash 结构能记录更多订单相关信息,便于进行复杂的查询和统计分析。在实际应用中,可根据业务需求灵活选择。
二、Redis的登场:为什么选择Redis?
Redis是一个高性能的内存数据库,支持多种数据结构,如字符串、哈希、列表、集合等。它的读写速度非常快,适合处理高并发的场景。因此,Redis成为了秒杀系统的首选。
三、乐观锁实现秒杀
3.1 乐观锁的原理
乐观锁认为冲突不一定会发生,所以在操作时再检查是否有冲突。Redis的WATCH命令就是一种乐观锁的实现。它会在事务执行前监视某个键,如果在事务执行期间该键被修改,事务就会失败。
3.2 Java实现乐观锁秒杀
我们先初始化库存,然后用乐观锁实现秒杀逻辑。
java
ini
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
public class SecKillWithOptimisticLock {
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
// 初始化库存
jedis.set("stock:1001", "100");
// 模拟用户秒杀
for (int i = 0; i < 200; i++) {
new Thread(() -> seckill("user" + Thread.currentThread().getId(), "1001")).start();
}
}
public static void seckill(String userId, String productId) {
Jedis jedis = new Jedis("localhost", 6379);
String stockKey = "stock:" + productId;
String orderKey = "order:" + productId;
// 监视库存
jedis.watch(stockKey);
int stock = Integer.parseInt(jedis.get(stockKey));
if (stock > 0) {
// 开启事务
Transaction tx = jedis.multi();
tx.decr(stockKey);
tx.rpush(orderKey, userId);
tx.exec();
System.out.println(userId + " 秒杀成功");
} else {
jedis.unwatch();
System.out.println(userId + " 秒杀失败");
}
jedis.close();
}
}
3.3 乐观锁的问题
乐观锁虽然简单,但在高并发场景下,事务失败的概率很高,导致大量请求重试,性能下降。这时候,我们需要更高效的方案。
四、Lua脚本实现秒杀
4.1 Lua脚本的优势
Lua脚本可以在Redis中原子执行,避免了乐观锁的重试问题。我们可以将秒杀逻辑封装到一个Lua脚本中,确保操作的原子性。
4.2 Lua脚本实现秒杀
我们编写一个Lua脚本,实现库存检查和订单生成的原子操作。
java
typescript
import redis.clients.jedis.Jedis;
public class SecKillWithLua {
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
// 初始化库存
jedis.set("stock:1001", "100");
// 加载Lua脚本
String script = "local stockKey = KEYS[1]\n" +
"local orderKey = KEYS[2]\n" +
"local userId = ARGV[1]\n" +
"local stock = tonumber(redis.call('get', stockKey))\n" +
"if stock > 0 then\n" +
" redis.call('decr', stockKey)\n" +
" redis.call('rpush', orderKey, userId)\n" +
" return '秒杀成功'\n" +
"else\n" +
" return '秒杀失败'\n" +
"end";
String scriptSha = jedis.scriptLoad(script);
// 模拟用户秒杀
for (int i = 0; i < 200; i++) {
new Thread(() -> seckill("user" + Thread.currentThread().getId(), "1001", scriptSha)).start();
}
}
public static void seckill(String userId, String productId, String scriptSha) {
Jedis jedis = new Jedis("localhost", 6379);
String stockKey = "stock:" + productId;
String orderKey = "order:" + productId;
// 执行Lua脚本
Object result = jedis.evalsha(scriptSha, 2, stockKey, orderKey, userId);
System.out.println(userId + " " + result);
jedis.close();
}
}
4.3 Lua脚本在集群环境下的问题
在Redis集群环境下,Lua脚本的执行可能会遇到问题。因为Lua脚本需要确保所有的键都在同一个节点上,否则会报错。为了解决这个问题,我们可以使用hash tag来确保相关的键分布在同一个节点上。
例如,我们可以将stock:1001和order:1001的键设计为{1001}:stock和{1001}:order,这样它们就会被分配到同一个节点。
五、Redisson分布式锁的解决方案
5.1 分布式锁的必要性
在分布式系统中,单机的锁机制无法满足需求。我们需要一个分布式锁来保证多个实例之间的互斥访问。
5.2 Redisson实现分布式锁
Redisson是一个基于Redis的Java客户端,提供了丰富的分布式对象和服务,包括分布式锁。
java
arduino
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class SecKillWithRedisson {
public static void main(String[] args) {
// 配置Redisson
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
RedissonClient redisson = Redisson.create(config);
// 初始化库存
redisson.getBucket("stock:1001").set(100);
// 模拟用户秒杀
for (int i = 0; i < 200; i++) {
new Thread(() -> seckill("user" + Thread.currentThread().getId(), "1001", redisson)).start();
}
}
public static void seckill(String userId, String productId, RedissonClient redisson) {
String stockKey = "stock:" + productId;
String orderKey = "order:" + productId;
// 获取分布式锁
RLock lock = redisson.getLock("lock:secKill");
lock.lock();
try {
int stock = (int) redisson.getBucket(stockKey).get();
if (stock > 0) {
redisson.getBucket(stockKey).set(stock - 1);
redisson.getList(orderKey).add(userId);
System.out.println(userId + " 秒杀成功");
} else {
System.out.println(userId + " 秒杀失败");
}
} finally {
lock.unlock();
}
}
}
5.3 Redisson分布式锁的优势
Redisson的分布式锁支持自动续期和可重入,能够有效避免死锁问题。此外,Redisson还提供了丰富的分布式对象和服务,如分布式集合、分布式队列等,非常适合构建复杂的分布式系统。
六、最佳实践
在实际应用中,我们可以结合多种方案来实现一个高效可靠的秒杀系统。比如,在前端进行限流,减少无效请求;在后端使用 Redis 进行缓存和分布式锁控制;使用 MQ(消息队列)来异步处理订单,减轻系统压力。同时,要对系统进行充分的性能测试和监控,及时发现和解决问题。就像建造一座大楼,每个环节都要精心设计和施工,才能保证大楼的稳固。
6.1 限流与降级
在高并发场景下,限流和降级是必不可少的。我们可以用Redis的INCR命令来实现简单的限流。比如,限制每秒最多处理1000个请求。
java
ini
public boolean limitRate(String userId) {
Jedis jedis = new Jedis("localhost", 6379);
String key = "rate:" + userId;
long count = jedis.incr(key);
if (count == 1) {
jedis.expire(key, 1);
}
jedis.close();
return count <= 1000;
}
6.2 异步处理订单
为了进一步提高系统的吞吐量,我们可以将订单处理异步化。比如,将订单信息写入消息队列,由后台任务处理。
java
java
import redis.clients.jedis.Jedis;
import java.util.UUID;
public class AsyncOrderProcessor {
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
// 模拟订单生成
for (int i = 0; i < 100; i++) {
String orderId = UUID.randomUUID().toString();
jedis.rpush("order:queue", orderId);
}
jedis.close();
}
}
七、总结
通过Redis,我们可以实现一个高性能的秒杀系统。从乐观锁到Lua脚本,再到Redisson分布式锁,我们一步步优化了系统的性能和可靠性。当然,这只是一个简单的实现,实际生产环境中还需要考虑更多的细节,比如数据库的读写分离、缓存的一致性、系统的监控与报警等。
最后,希望大家在实现秒杀系统的过程中,不要放弃,不要崩溃,保持乐观,毕竟,程序员的生活就是在崩溃的边缘疯狂试探。
八、思考题
- 除了Redis,还有哪些技术可以用于实现秒杀系统?
- 在高并发场景下,如何保证缓存与数据库的一致性?
- Lua脚本在Redis集群环境下会遇到什么问题?如何解决?
希望这篇文章能给你带来一些启发,也欢迎大家在评论区分享你的想法和经验。我们下次再见!