使用redis扣减库存,保证库存不超发或少发的核心是保证redis读取库存和扣减库存的原子性。不能多个线程读取同一时刻的库存再去做扣减,也不能多个线程先扣减库存再去读库存。
下面先介绍正确实例
1.使用lua脚本
SpringDataRedis版本 代码如下
package com.wl.redis.jedis;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* Created by wl on 2021/6/16.
*/
@Component
public class RedisStock implements InitializingBean{
private StringRedisTemplate stringRedisTemplate;
@Autowired
public RedisStock(StringRedisTemplate stringRedisTemplate){
this.stringRedisTemplate = stringRedisTemplate;
}
public boolean decrementStockWithLuaRedisTemplate(){
String script = "local stock = redis.call('get',KEYS[1]); local decrementNumber = tonumber(ARGV[1]);" +
" if tonumber(stock) + decrementNumber >= 0 then redis.call('INCRBY',KEYS[1],decrementNumber) return 1;" +
" end return 0;";
List<String> keys = new ArrayList<>();
RedisScript redisScript = new DefaultRedisScript<>(script,Long.class);
keys.add("stock");
//-1表示decrementNumber 根据自己的扣减库存量自行设置
Object[] args = {"-1"};
Long result = (Long) stringRedisTemplate.execute(redisScript,keys,args);
return result == 1L;
}
@Override
public void afterPropertiesSet() throws Exception {
stringRedisTemplate.opsForValue().set("stock", "100");
}
}
测试代码如下(开启六个线程调用扣减库存的方法)
package com.wl.redis;
import com.wl.redis.jedis.RedisStock;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Created by wl on 2021/6/16.
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = Application.class)
public class RedisStockTest {
@Autowired
private RedisStock redisStock;
@Autowired
private StringRedisTemplate stringRedisTemplate;
private AtomicInteger count = new AtomicInteger(0);
@Test
public void testDecrementStockWithLuaRedisTemplate() throws Exception{
CountDownLatch countDownLatch = new CountDownLatch(6);
List<Thread> list = new ArrayList<>();
for (int i=0;i<6;i++){
list.add(new Thread(() -> {
while (true){
if(!redisStock.decrementStockWithLuaRedisTemplate()){
countDownLatch.countDown();
break;
}else{
count.addAndGet(1);
}
}
}));
}
for(int i=0;i<6;i++){
list.get(i).start();
}
countDownLatch.await();
System.out.println("扣减库存返回true的次数:" + count.get());
System.out.println("剩余库存数量:" + stringRedisTemplate.opsForValue().get("stock"));
}
}
执行结果如下(执行耗时2秒)
2021-06-16 23:25:31.632 INFO 14764 --- [ main] com.wl.redis.RedisStockTest : Started RedisStockTest in 11.023 seconds (JVM running for 12.999)
扣减库存返回true的次数:100
剩余库存数量:0
Jedis版本代码
private JedisPool pool = new JedisPool(new GenericObjectPoolConfig(),"localhost",6379,3000,"password",0);
public boolean decrementStockWithLuaJedis(){
String script = "local stock = redis.call('get',KEYS[1]); local decrementNumber = tonumber(ARGV[1]);" +
" if tonumber(stock) + decrementNumber >= 0 then redis.call('INCRBY',KEYS[1],decrementNumber) return 1;" +
" end return 0;";
List<String> keys = new ArrayList<>();
keys.add("stock");
//-1表示decrementNumber 根据自己的扣减库存量自行设置
String[] args = {"-1"};
Jedis jedis = pool.getResource();
try {
Long result = (Long) jedis.eval(script, keys, Arrays.asList(args));
return result == 1L;
}catch (Exception e){
//
}finally {
jedis.close();
}
return false;
}
测试代码
@Test
public void testDecrementStockWithLuaJedis() throws Exception{
CountDownLatch countDownLatch = new CountDownLatch(6);
List<Thread> list = new ArrayList<>();
for (int i=0;i<6;i++){
list.add(new Thread(() -> {
while (true){
if(!redisStock.decrementStockWithLuaJedis()){
countDownLatch.countDown();
break;
}else{
count.addAndGet(1);
}
}
}));
}
for(int i=0;i<6;i++){
list.get(i).start();
}
countDownLatch.await();
System.out.println("扣减库存返回true的次数:" + count.get());
System.out.println("剩余库存数量:" + stringRedisTemplate.opsForValue().get("stock"));
}
执行结果(执行耗时也是不到两秒钟)
2021-06-16 23:29:29.649 INFO 13464 --- [ main] com.wl.redis.RedisStockTest : Started RedisStockTest in 9.271 seconds (JVM running for 11.772)
扣减库存返回true的次数:100
剩余库存数量:0
2.使用redis分布式锁保证原子性 代码如下
public boolean decrementStockWithLock(){
boolean lock = lock("stock_lock");
if(lock){
try{
String stockStr = stringRedisTemplate.opsForValue().get("stock");
Integer stock = Integer.parseInt(stockStr);
if (stock > 0){
stringRedisTemplate.opsForValue().increment("stock",-1L);
return true;
}
return false;
}finally {
unlock("stock_lock");
}
}
return false;
}
private boolean lock(String lockKey){
long timeout = 30000;
long start = System.currentTimeMillis();
while (true){
Boolean b = stringRedisTemplate.execute(new SessionCallback<Boolean>() {
List<Object> exec = null;
@Override
@SuppressWarnings("unchecked")
public Boolean execute(RedisOperations operations) throws DataAccessException {
operations.multi();
operations.opsForValue().setIfAbsent(lockKey,"lock");
operations.expire(lockKey, 30, TimeUnit.SECONDS);
exec = operations.exec();
if(exec.size() > 0) {
return (Boolean) exec.get(0);
}
return false;
}
});
if(b){
return true;
}
long l = System.currentTimeMillis() - start;
//超时未获取锁直接返回false
if (l>=timeout) {
return false;
}
try {
//未获取锁时每100毫秒尝试获取锁
Thread.sleep(100);
} catch (InterruptedException e) {
//
}
}
}
private void unlock(String lockKey){
stringRedisTemplate.delete(lockKey);
}
测试代码
@Test
public void testDecrementStockWithLock() throws Exception{
CountDownLatch countDownLatch = new CountDownLatch(6);
List<Thread> list = new ArrayList<>();
for (int i=0;i<6;i++){
list.add(new Thread(() -> {
while (true){
if(!redisStock.decrementStockWithLock()){
countDownLatch.countDown();
break;
}else{
count.addAndGet(1);
}
}
}));
}
for(int i=0;i<6;i++){
list.get(i).start();
}
countDownLatch.await();
System.out.println("扣减库存返回true的次数:" + count.get());
System.out.println("剩余库存数量:" + stringRedisTemplate.opsForValue().get("stock"));
}
执行结果(耗时30多秒,去掉获取锁的100毫秒等待时间,耗时仍然需要20多秒)
2021-06-16 23:31:51.655 INFO 14884 --- [ main] com.wl.redis.RedisStockTest : Started RedisStockTest in 7.993 seconds (JVM running for 9.674)
扣减库存返回true的次数:100
剩余库存数量:0
3.使用redis事物multi 代码如下(只适用于每次扣减一个库存的情景)
public boolean decrementStockWithMulti(){
String stockStr = stringRedisTemplate.opsForValue().get("stock");
if(Integer.parseInt(stockStr) <= 0){
return false;
}
return stringRedisTemplate.execute(new SessionCallback<Boolean>() {
@Override
@SuppressWarnings("unchecked")
public Boolean execute(RedisOperations operations) throws DataAccessException {
operations.multi();
operations.opsForValue().increment("stock",-1L);
operations.opsForValue().get("stock");
List<Object> result = operations.exec();
String afterStockStr = result.get(1).toString();
return Integer.parseInt(afterStockStr) >= 0;
}
});
}
测试代码
@Test
public void testDecrementStockWithMulti() throws Exception{
CountDownLatch countDownLatch = new CountDownLatch(6);
List<Thread> list = new ArrayList<>();
for (int i=0;i<6;i++){
list.add(new Thread(() -> {
while (true){
if(!redisStock.decrementStockWithMulti()){
countDownLatch.countDown();
break;
}else{
count.addAndGet(1);
}
}
}));
}
for(int i=0;i<6;i++){
list.get(i).start();
}
countDownLatch.await();
System.out.println("扣减库存返回true的次数:" + count.get());
System.out.println("剩余库存数量:" + stringRedisTemplate.opsForValue().get("stock"));
}
执行结果(执行耗时7秒钟) 可以看到虽然库存最后是负数,但是返回扣减成功的次数是正确的100次。因为库存被扣减完后,还有线程在继续扣减,不过返回调用者扣减结果是false
2021-06-16 23:38:58.510 INFO 2172 --- [ main] com.wl.redis.RedisStockTest : Started RedisStockTest in 8.442 seconds (JVM running for 10.261)
扣减库存返回true的次数:100
剩余库存数量:-4
下面是错误示例
4.先查询库存在扣减库存(这种情况会导致库存多发)
public boolean getAndDecrementStock(){
String stockStr = stringRedisTemplate.opsForValue().get("stock");
Integer stock = Integer.parseInt(stockStr);
if (stock > 0){
stringRedisTemplate.opsForValue().increment("stock",-1L);
return true;
}
return false;
}
测试代码如下
@Test
public void testDecrementStock() throws Exception{
CountDownLatch countDownLatch = new CountDownLatch(6);
List<Thread> list = new ArrayList<>();
for (int i=0;i<6;i++){
list.add(new Thread(() -> {
while (true){
if(!redisStock.getAndDecrementStock()){
countDownLatch.countDown();
break;
}else{
count.addAndGet(1);
}
}
}));
}
for(int i=0;i<6;i++){
list.get(i).start();
}
countDownLatch.await();
System.out.println("扣减库存返回true的次数:" + count.get());
System.out.println("剩余库存数量:" + stringRedisTemplate.opsForValue().get("stock"));
}
执行结果如下
2021-06-16 23:46:11.455 INFO 12612 --- [ main] com.wl.redis.RedisStockTest : Started RedisStockTest in 7.792 seconds (JVM running for 9.404)
扣减库存返回true的次数:102
剩余库存数量:-2
5.先扣减库存在查询库存(会导致库存少发)
public boolean decrementAndGetStock(){
stringRedisTemplate.opsForValue().increment("stock",-1L);
String stockStr = stringRedisTemplate.opsForValue().get("stock");
Integer stock = Integer.parseInt(stockStr);
if (stock > 0){
return true;
}
return false;
}
测试代码如下
@Test
public void testDecrementAndGetStock() throws Exception{
CountDownLatch countDownLatch = new CountDownLatch(6);
List<Thread> list = new ArrayList<>();
for (int i=0;i<6;i++){
list.add(new Thread(() -> {
while (true){
if(!redisStock.decrementAndGetStock()){
countDownLatch.countDown();
break;
}else{
count.addAndGet(1);
}
}
}));
}
for(int i=0;i<6;i++){
list.get(i).start();
}
countDownLatch.await();
System.out.println("扣减库存返回true的次数:" + count.get());
System.out.println("剩余库存数量:" + stringRedisTemplate.opsForValue().get("stock"));
}
执行结果
2021-06-16 23:48:08.875 INFO 6884 --- [ main] com.wl.redis.RedisStockTest : Started RedisStockTest in 8.242 seconds (JVM running for 10.781)
扣减库存返回true的次数:96
剩余库存数量:-2
完整代码如下
package com.wl.redis.jedis;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* Created by wl on 2021/6/16.
*/
@Component
public class RedisStock implements InitializingBean{
private StringRedisTemplate stringRedisTemplate;
@Autowired
public RedisStock(StringRedisTemplate stringRedisTemplate){
this.stringRedisTemplate = stringRedisTemplate;
}
public boolean decrementStockWithLuaRedisTemplate(){
String script = "local stock = redis.call('get',KEYS[1]); local decrementNumber = tonumber(ARGV[1]);" +
" if tonumber(stock) + decrementNumber >= 0 then redis.call('INCRBY',KEYS[1],decrementNumber) return 1;" +
" end return 0;";
List<String> keys = new ArrayList<>();
RedisScript redisScript = new DefaultRedisScript<>(script,Long.class);
keys.add("stock");
//-1表示decrementNumber 根据自己的扣减库存量自行设置
Object[] args = {"-1"};
Long result = (Long) stringRedisTemplate.execute(redisScript,keys,args);
return result == 1L;
}
private JedisPool pool = new JedisPool(new GenericObjectPoolConfig(),"localhost",6379,3000,"password",0);
public boolean decrementStockWithLuaJedis(){
String script = "local stock = redis.call('get',KEYS[1]); local decrementNumber = tonumber(ARGV[1]);" +
" if tonumber(stock) + decrementNumber >= 0 then redis.call('INCRBY',KEYS[1],decrementNumber) return 1;" +
" end return 0;";
List<String> keys = new ArrayList<>();
keys.add("stock");
//-1表示decrementNumber 根据自己的扣减库存量自行设置
String[] args = {"-1"};
Jedis jedis = pool.getResource();
try {
Long result = (Long) jedis.eval(script, keys, Arrays.asList(args));
return result == 1L;
}catch (Exception e){
//
}finally {
jedis.close();
}
return false;
}
public boolean decrementStockWithLock(){
boolean lock = lock("stock_lock");
if(lock){
try{
String stockStr = stringRedisTemplate.opsForValue().get("stock");
Integer stock = Integer.parseInt(stockStr);
if (stock > 0){
stringRedisTemplate.opsForValue().increment("stock",-1L);
return true;
}
return false;
}finally {
unlock("stock_lock");
}
}
return false;
}
private boolean lock(String lockKey){
long timeout = 30000;
long start = System.currentTimeMillis();
while (true){
Boolean b = stringRedisTemplate.execute(new SessionCallback<Boolean>() {
List<Object> exec = null;
@Override
@SuppressWarnings("unchecked")
public Boolean execute(RedisOperations operations) throws DataAccessException {
operations.multi();
operations.opsForValue().setIfAbsent(lockKey,"lock");
operations.expire(lockKey, 30, TimeUnit.SECONDS);
exec = operations.exec();
if(exec.size() > 0) {
return (Boolean) exec.get(0);
}
return false;
}
});
if(b){
return true;
}
long l = System.currentTimeMillis() - start;
//超时未获取锁直接返回false
if (l>=timeout) {
return false;
}
try {
//未获取锁时每100毫秒尝试获取锁
Thread.sleep(100);
} catch (InterruptedException e) {
//
}
}
}
private void unlock(String lockKey){
stringRedisTemplate.delete(lockKey);
}
public boolean decrementStockWithMulti(){
String stockStr = stringRedisTemplate.opsForValue().get("stock");
if(Integer.parseInt(stockStr) <= 0){
return false;
}
return stringRedisTemplate.execute(new SessionCallback<Boolean>() {
@Override
@SuppressWarnings("unchecked")
public Boolean execute(RedisOperations operations) throws DataAccessException {
operations.multi();
operations.opsForValue().increment("stock",-1L);
operations.opsForValue().get("stock");
List<Object> result = operations.exec();
String afterStockStr = result.get(1).toString();
return Integer.parseInt(afterStockStr) >= 0;
}
});
}
//减库存 没有锁
public boolean getAndDecrementStock(){
String stockStr = stringRedisTemplate.opsForValue().get("stock");
Integer stock = Integer.parseInt(stockStr);
if (stock > 0){
stringRedisTemplate.opsForValue().increment("stock",-1L);
return true;
}
return false;
}
public boolean decrementAndGetStock(){
stringRedisTemplate.opsForValue().increment("stock",-1L);
String stockStr = stringRedisTemplate.opsForValue().get("stock");
Integer stock = Integer.parseInt(stockStr);
if (stock > 0){
return true;
}
return false;
}
@Override
public void afterPropertiesSet() throws Exception {
stringRedisTemplate.opsForValue().set("stock", "100");
}
}
完整测试代码如下
package com.wl.redis;
import com.wl.redis.jedis.RedisStock;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Created by wl on 2021/6/16.
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = Application.class)
public class RedisStockTest {
@Autowired
private RedisStock redisStock;
@Autowired
private StringRedisTemplate stringRedisTemplate;
private AtomicInteger count = new AtomicInteger(0);
//===================================================正确示例=======================================
@Test
public void testDecrementStockWithLuaRedisTemplate() throws Exception{
CountDownLatch countDownLatch = new CountDownLatch(6);
List<Thread> list = new ArrayList<>();
for (int i=0;i<6;i++){
list.add(new Thread(() -> {
while (true){
if(!redisStock.decrementStockWithLuaRedisTemplate()){
countDownLatch.countDown();
break;
}else{
count.addAndGet(1);
}
}
}));
}
for(int i=0;i<6;i++){
list.get(i).start();
}
countDownLatch.await();
System.out.println("扣减库存返回true的次数:" + count.get());
System.out.println("剩余库存数量:" + stringRedisTemplate.opsForValue().get("stock"));
}
@Test
public void testDecrementStockWithLuaJedis() throws Exception{
CountDownLatch countDownLatch = new CountDownLatch(6);
List<Thread> list = new ArrayList<>();
for (int i=0;i<6;i++){
list.add(new Thread(() -> {
while (true){
if(!redisStock.decrementStockWithLuaJedis()){
countDownLatch.countDown();
break;
}else{
count.addAndGet(1);
}
}
}));
}
for(int i=0;i<6;i++){
list.get(i).start();
}
countDownLatch.await();
System.out.println("扣减库存返回true的次数:" + count.get());
System.out.println("剩余库存数量:" + stringRedisTemplate.opsForValue().get("stock"));
}
@Test
public void testDecrementStockWithLock() throws Exception{
CountDownLatch countDownLatch = new CountDownLatch(6);
List<Thread> list = new ArrayList<>();
for (int i=0;i<6;i++){
list.add(new Thread(() -> {
while (true){
if(!redisStock.decrementStockWithLock()){
countDownLatch.countDown();
break;
}else{
count.addAndGet(1);
}
}
}));
}
for(int i=0;i<6;i++){
list.get(i).start();
}
countDownLatch.await();
System.out.println("扣减库存返回true的次数:" + count.get());
System.out.println("剩余库存数量:" + stringRedisTemplate.opsForValue().get("stock"));
}
//==========================================正确示例,但是只适用每次减一个库存的情景====================
@Test
public void testDecrementStockWithMulti() throws Exception{
CountDownLatch countDownLatch = new CountDownLatch(6);
List<Thread> list = new ArrayList<>();
for (int i=0;i<6;i++){
list.add(new Thread(() -> {
while (true){
if(!redisStock.decrementStockWithMulti()){
countDownLatch.countDown();
break;
}else{
count.addAndGet(1);
}
}
}));
}
for(int i=0;i<6;i++){
list.get(i).start();
}
countDownLatch.await();
System.out.println("扣减库存返回true的次数:" + count.get());
System.out.println("剩余库存数量:" + stringRedisTemplate.opsForValue().get("stock"));
}
//=========================================错误示例=============================================
//测试出现了 stock =-2
@Test
public void testDecrementStock() throws Exception{
CountDownLatch countDownLatch = new CountDownLatch(6);
List<Thread> list = new ArrayList<>();
for (int i=0;i<6;i++){
list.add(new Thread(() -> {
while (true){
if(!redisStock.getAndDecrementStock()){
countDownLatch.countDown();
break;
}else{
count.addAndGet(1);
}
}
}));
}
for(int i=0;i<6;i++){
list.get(i).start();
}
countDownLatch.await();
System.out.println("扣减库存返回true的次数:" + count.get());
System.out.println("剩余库存数量:" + stringRedisTemplate.opsForValue().get("stock"));
}
@Test
public void testDecrementAndGetStock() throws Exception{
CountDownLatch countDownLatch = new CountDownLatch(6);
List<Thread> list = new ArrayList<>();
for (int i=0;i<6;i++){
list.add(new Thread(() -> {
while (true){
if(!redisStock.decrementAndGetStock()){
countDownLatch.countDown();
break;
}else{
count.addAndGet(1);
}
}
}));
}
for(int i=0;i<6;i++){
list.get(i).start();
}
countDownLatch.await();
System.out.println("扣减库存返回true的次数:" + count.get());
System.out.println("剩余库存数量:" + stringRedisTemplate.opsForValue().get("stock"));
}
}
redis 版本
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.0.8.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.3.0</version>
</dependency>
最后
以上就是淡淡白云最近收集整理的关于redis扣减库存示例代码 java的全部内容,更多相关redis扣减库存示例代码内容请搜索靠谱客的其他文章。
本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
发表评论 取消回复