加锁使用命令:set lock_key unique_value NX PX 1000
NX:等同于SETNX ,只有键不存在时才能设置成功
PX:设置键的过期时间为10秒
unique_value:一个必须是唯一的随机值(UUID),通常由客户端生成。解决误删他人锁的关键。
这条命令是原子性的,要么一起成功,要么一起失败。
解锁:Lua 脚本保证原子性
需要先判断当前锁的值是否是自己设置的unique_value,如果是,才能使用DEL删除,两个操作必须保证原子性,使用Lua脚本安全的释放锁;
// unlock.lua
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
下面是一个完整的基于 Spring Boot 和 Vue 的秒杀案例,使用 Redis 分布式锁防止超卖。
后端实现 (Spring Boot)
1. 添加依赖 (pom.xml)
XML
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
</dependencies>
2. 应用配置 (application.yml)
XML
spring:
redis:
host: localhost
port: 6379
password:
database: 0
lettuce:
pool:
max-active: 8
max-wait: -1ms
max-idle: 8
min-idle: 0
server:
port: 8080
3. Redis 分布式锁工具类
java
@Component
public class RedisDistributedLock {
@Autowired
private StringRedisTemplate redisTemplate;
// 锁的超时时间,防止死锁
private static final long LOCK_EXPIRE = 30000L; // 30秒
// 获取锁的等待时间
private static final long LOCK_WAIT_TIME = 3000L; // 3秒
// 锁的重试间隔
private static final long SLEEP_TIME = 100L; // 100毫秒
/**
* 尝试获取分布式锁
* @param lockKey 锁的key
* @param requestId 请求标识(可以使用UUID)
* @param expireTime 锁的超时时间(毫秒)
* @return 是否获取成功
*/
public boolean tryLock(String lockKey, String requestId, long expireTime) {
try {
long startTime = System.currentTimeMillis();
while (true) {
// 使用SET命令代替SETNX,保证原子性
Boolean result = redisTemplate.opsForValue().setIfAbsent(
lockKey,
requestId,
expireTime,
TimeUnit.MILLISECONDS
);
if (Boolean.TRUE.equals(result)) {
return true; // 获取锁成功
}
// 检查是否超时
if (System.currentTimeMillis() - startTime > LOCK_WAIT_TIME) {
return false; // 获取锁超时
}
// 等待一段时间后重试
try {
Thread.sleep(SLEEP_TIME);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
} catch (Exception e) {
return false;
}
}
/**
* 释放分布式锁
* @param lockKey 锁的key
* @param requestId 请求标识
* @return 是否释放成功
*/
public boolean releaseLock(String lockKey, String requestId) {
// 使用Lua脚本保证原子性 ,先判断锁的键值是否等于requestId,等于才能进行删除
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else " +
"return 0 " +
"end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(script);
redisScript.setResultType(Long.class);
Long result = redisTemplate.execute(redisScript,
Collections.singletonList(lockKey),
requestId);
return result != null && result == 1;
}
/**
* 简化版获取锁(使用默认超时时间)
*/
public boolean tryLock(String lockKey, String requestId) {
return tryLock(lockKey, requestId, LOCK_EXPIRE);
}
}
4. 商品服务类
java
@Service
public class ProductService {
@Autowired
private RedisDistributedLock redisDistributedLock;
@Autowired
private StringRedisTemplate redisTemplate;
private static final String PRODUCT_STOCK_PREFIX = "product:stock:";
private static final String PRODUCT_LOCK_PREFIX = "product:lock:";
/**
* 初始化商品库存 //从数据库中查询出对应商品的库存数量
*/
public void initProductStock(Long productId, Integer stock) {
redisTemplate.opsForValue().set(PRODUCT_STOCK_PREFIX + productId, stock.toString());
}
/**
* 获取商品库存
*/
public Integer getProductStock(Long productId) {
String stockStr = redisTemplate.opsForValue().get(PRODUCT_STOCK_PREFIX + productId);
return stockStr != null ? Integer.parseInt(stockStr) : 0;
}
/**
* 秒杀下单(使用分布式锁)
*/
public boolean seckillProduct(Long productId, String userId) {
String lockKey = PRODUCT_LOCK_PREFIX + productId;
String requestId = UUID.randomUUID().toString();
try {
// 尝试获取锁
if (!redisDistributedLock.tryLock(lockKey, requestId)) {
return false; // 获取锁失败
}
// 检查库存
Integer stock = getProductStock(productId);
if (stock <= 0) {
return false; // 库存不足
}
// 模拟业务处理耗时 //修改商品的库存
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 扣减库存
redisTemplate.opsForValue().decrement(PRODUCT_STOCK_PREFIX + productId);
// 记录订单(这里简化处理,实际应保存到数据库)
System.out.println("用户 " + userId + " 成功秒杀商品 " + productId);
return true;
} finally {
// 释放锁
redisDistributedLock.releaseLock(lockKey, requestId);
}
}
}
前端实现 (Vue)
1. 安装依赖
java
npm install axios
2. 秒杀页面组件 (Seckill.vue)
java
<template>
<div class="seckill-container">
<h1>商品秒杀</h1>
<div class="product-info">
<h2>商品ID: {{ productId }}</h2>
<p>当前库存: {{ stock }}</p>
<button @click="initStock">初始化库存(100件)</button>
</div>
<div class="seckill-form">
<input v-model="userId" placeholder="请输入用户ID" />
<button @click="seckill" :disabled="isSeckilling">
{{ isSeckilling ? '秒杀中...' : '立即秒杀' }}
</button>
</div>
<div class="result">
<h3>秒杀结果:</h3>
<p>{{ resultMessage }}</p>
</div>
<div class="logs">
<h3>操作日志:</h3>
<ul>
<li v-for="(log, index) in logs" :key="index">{{ log }}</li>
</ul>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'Seckill',
data() {
return {
productId: 1001, // 商品ID
stock: 0, // 当前库存
userId: '', // 用户ID
resultMessage: '', // 秒杀结果
logs: [], // 操作日志
isSeckilling: false // 是否正在秒杀
};
},
mounted() {
this.getStock();
},
methods: {
// 获取商品库存
async getStock() {
try {
const response = await axios.get(`http://localhost:8080/api/seckill/stock/${this.productId}`);
this.stock = response.data;
this.addLog(`获取库存成功: ${this.stock}`);
} catch (error) {
this.addLog('获取库存失败: ' + error.message);
}
},
// 初始化库存
async initStock() {
try {
await axios.post(`http://localhost:8080/api/seckill/init/${this.productId}/100`);
this.addLog('初始化库存成功');
this.getStock(); // 重新获取库存
} catch (error) {
this.addLog('初始化库存失败: ' + error.message);
}
},
// 执行秒杀
async seckill() {
if (!this.userId) {
this.resultMessage = '请输入用户ID';
return;
}
this.isSeckilling = true;
this.resultMessage = '秒杀中...';
try {
const response = await axios.post(
`http://localhost:8080/api/seckill/${this.productId}?userId=${this.userId}`
);
this.resultMessage = response.data;
this.addLog(`用户 ${this.userId} ${response.data}`);
} catch (error) {
this.resultMessage = '秒杀失败: ' + (error.response?.data || error.message);
this.addLog(`用户 ${this.userId} 秒杀失败: ${error.response?.data || error.message}`);
} finally {
this.isSeckilling = false;
this.getStock(); // 重新获取库存
}
},
// 添加日志
addLog(message) {
const timestamp = new Date().toLocaleTimeString();
this.logs.unshift(`[${timestamp}] ${message}`);
// 只保留最近20条日志
if (this.logs.length > 20) {
this.logs.pop();
}
}
}
};
</script>
<style scoped>
.seckill-container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.product-info, .seckill-form, .result, .logs {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
}
input {
padding: 8px;
margin-right: 10px;
width: 200px;
}
button {
padding: 8px 16px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
button:hover:not(:disabled) {
background-color: #45a049;
}
ul {
list-style-type: none;
padding: 0;
max-height: 300px;
overflow-y: auto;
}
li {
padding: 5px 0;
border-bottom: 1px solid #eee;
}
</style>
3. 主应用文件 (App.vue)
java
<template>
<div id="app">
<Seckill />
</div>
</template>
<script>
import Seckill from './components/Seckill.vue'
export default {
name: 'App',
components: {
Seckill
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
margin-top: 20px;
}
</style>
-
原子性加锁 :使用
setIfAbsent
方法的原子性操作,避免非原子操作带来的竞态条件 -
唯一请求标识:使用 UUID 作为请求标识,确保只能释放自己加的锁
-
超时机制:设置锁的超时时间,防止死锁
-
Lua脚本释放锁:使用 Lua 脚本保证判断锁归属和删除操作的原子性
-
重试机制:在获取锁失败后等待一段时间重试,避免立即失败