为解决在秒杀时避免使用分布式锁来解决多条redis命令同时执行问题,那最好的办法就是使用lua脚本,使多条操作数据的命令是原子化的并且在具有较高的执行速度的同时不影响其它线程。
下面两个例子分别为单个商品进行库减和同时多个商品进行库减
依赖项,springboot依赖项就不在此展示了根据自身情况而定:
<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>
<!-- 工具库 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.22</version>
</dependency>
实战代码示例:
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.script.RedisScript;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@SpringBootTest
public class LuaTests {
@Resource(name = "redisTemplate")
ValueOperations<String, Long> valueOperations;
@Autowired
StringRedisTemplate stringRedisTemplate;
/**
* 单商品下单
*/
@Test
void test() throws IOException {
final String productKey = "product.1";
StringBuilder sb = new StringBuilder();
sb.append(" local key = KEYS[1] ");
sb.append(" local qty = ARGV[1] ");
sb.append(" local redis_qty = redis.call('get',key) ");
sb.append(" if tonumber(redis_qty) >= tonumber(qty) then ");
sb.append(" redis.call('decrby',key,qty) ");
sb.append(" return -1 "); //-1代表扣减成功
sb.append(" else ");
sb.append(" return tonumber(redis_qty) "); //返回当前库存量
sb.append(" end ");
RedisScript<Long> script = RedisScript.of(sb.toString(), Long.class);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
executorService.execute(() -> {
int c = RandomUtil.randomInt(1, 5);
Long count = stringRedisTemplate.execute(script, CollUtil.newArrayList(productKey), c + "");
if (count == -1) {
System.out.println(StrUtil.format("线程{}扣减 {} 成功", Thread.currentThread().getId(), c));
} else {
System.out.println(StrUtil.format("线程{}扣减 {} 失败,库存剩余{}", Thread.currentThread().getId(), c, count));
}
});
}
//防止主线程退出
System.in.read();
}
/**
* 多商品同时下单
*/
@Test
void test1() throws IOException {
//准备模拟数据
final String product1 = "product.1";
final String product2 = "product.2";
final String product3 = "product.3";
valueOperations.set(product1, 10L);
valueOperations.set(product2, 15L);
valueOperations.set(product3, 20L);
//编写Lua脚本
StringBuilder sb = new StringBuilder();
//准备talbe数组,将库存不足商品放入其中当做返回值
sb.append(" local table = {} ");
//根据商品数组查出对应库存
sb.append(" local redis_qtys = redis.call('mget',unpack(KEYS)) ");
//逐一判断要购买商品库存是否充足
sb.append(" for i=1, #KEYS do ");
sb.append(" if tonumber(ARGV[i]) > tonumber(redis_qtys[i]) then ");
sb.append(" table[#table+1] = KEYS[i] .. '=' .. redis_qtys[i] ");
sb.append(" end ");
sb.append(" end ");
sb.append(" if #table > 0 then");
sb.append(" return table ");
sb.append(" end ");
sb.append(" for i=1, #KEYS do ");
sb.append(" redis.call('decrby',KEYS[i],ARGV[i]) ");
sb.append(" end ");
sb.append(" return table ");
RedisScript<List> script = RedisScript.of(sb.toString(), List.class);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
executorService.execute(() -> {
//模拟每样商品要买的数量
int needQty1 = RandomUtil.randomInt(1, 5);
int needQty2 = RandomUtil.randomInt(1, 5);
int needQty3 = RandomUtil.randomInt(1, 5);
ArrayList<String> list = CollUtil.newArrayList(product1, product2, product3);
List execute = stringRedisTemplate.execute(script, list, needQty1 + "", needQty2 + "", needQty3 + "");
if (execute.size() == 0) {
System.out.println(StrUtil.format("线程{}扣减product1={},product2={},product3={} 成功", Thread.currentThread().getId(), needQty1, needQty2, needQty3));
}else {
System.out.println(StrUtil.format("线程{}扣减{}失败", Thread.currentThread().getId(),execute));
}
});
}
//防止主线程退出
System.in.read();
}
}