Redis Lua脚本解决秒杀下库存校验问题

Redis Lua脚本解决秒杀下库存校验问题

场景

基本需求

秒杀活动,到时间点后,用户会对商品进行购买。

涉及问题

  • 秒杀场景下,瞬时的并发会比较高
  • 商品的数量是有限的,不能超买超卖
  • 每个用户最多只能抢购一件商品

综合上面问题,传统关系型数据库不能良好的支持。

解决思路

主要流程

Created with Raphaël 2.2.0 开始 提交秒杀请求 扣减库存 跳转下单 订单成功 结束 增加库存 yes no yes no

这里是解决提交秒杀请求后,如何设计扣减库存的实现。

使用redis lua脚本

为什么使用Redis Lua

  • Redis为单线程模型,Lua脚本执行具有原子性
  • Redis可以支持高并发访问

Lua脚本设计

-- KEYS [good]
-- ARGV [uid]
-- return -1-库存不足 0-重复购买 1-成功

local good = KEYS[1]
local activity = ARGV[1]
local uid = ARGV[2]
local gooduids = good .. ':' .. activity .. ':uids'

local isin = redis.call('SISMEMBER', gooduids, uid)

if isin > 0 then
  return 0
end

local goodstock = good .. ':' .. activity .. ':stock'
local stock = redis.call('GET', goodstock)

if not stock or tonumber(stock) <= 0 then
  return -1
end

redis.call('DECR', goodstock)
redis.call('SADD', gooduids, uid)
return 1

说明

使用Redis EVAL命令执行
EVAL script numkeys key [key …] arg [arg …]
如 eval “…” 2 goodid activityid uid

Created with Raphaël 2.2.0 开始 用户是否参加过 返回0 结束 是否还有库存 扣减库存 添加用户信息 返回1 结束 返回-1 结束 yes no yes no

  • 从参数中获取good编号,activity编号,uid。
  • 使用SISMEMBER命令,判断“good:activity:uids”set中,是否有uid,有表示参加过秒杀,返回0
  • 使用GET命令,查询“good:activity:stock”获取库存,判断是否还有库存,库存小于等于0,返回-1
  • 使用DECR命令,对“good:activity:stock”减一,扣减库存,使用SADD命令添加“good:activity:uids”用户uid,返回1

java测试代码

使用Jedis作为驱动

mport redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class JedisLua { 

    static final JedisPool pool;
    static { 
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(100);
        pool = new JedisPool(config, "127.0.0.1");
        Runtime.getRuntime().addShutdownHook(new Thread(()->pool.close()));
    }

    /** * 重置商品的库存 * @param good * @param activity * @param stock */
    public static final void reset(String good, String activity, int stock) { 
        Jedis jedis = pool.getResource();
        try { 
            jedis.set(good + ":" + activity + ":stock", stock + "");
            jedis.del(good + ":" + activity + ":uids");
        } finally { 
            if (jedis != null) { 
                jedis.close();
            }
        }
    }

    public static final String RUSH_TO_BUY_LUA =
            "local good = KEYS[1]\n" +
            "local uid = ARGV[1]\n" +
            "local activity = KEYS[2]\n" +
            "local gooduids = good .. ':' .. activity .. ':uids'\n" +
            "\n" +
            "local isin = redis.call('SISMEMBER', gooduids, uid)\n" +
            "\n" +
            "if isin > 0 then\n" +
            " return 0\n" +
            "end\n" +
            "\n" +
            "local goodstock = good .. ':' .. activity .. ':stock'\n" +
            "local stock = redis.call('GET', goodstock)\n" +
            "\n" +
            "if not stock or tonumber(stock) <= 0 then\n" +
            " return -1\n" +
            "end\n" +
            "\n" +
            "redis.call('DECR', goodstock)\n" +
            "redis.call('SADD', gooduids, uid)\n" +
            "return 1";

    /** * 加载lua脚本到redis中 * @return */
    public static String rushToBuySHA1() { 
        Jedis jedis = pool.getResource();
        try { 
            return jedis.scriptLoad(RUSH_TO_BUY_LUA);
        } finally { 
            if (jedis != null) { 
                jedis.close();
            }
        }
    }

    /** * * @param good 商品编号 * @param activity 活动编号 * @param uid 用户id * @param scriptsha1 redis lua 脚本sha1值 * @return */
    public static int rushToBuy(String good, String activity, String uid, String scriptsha1) { 
        Jedis jedis = pool.getResource();
        try { 
            if (scriptsha1 != null) { 
                return ((Long) jedis.evalsha(scriptsha1, Arrays.asList(good, activity), Arrays.asList(uid))).intValue();
            } else { 
                return ((Long) jedis.eval(RUSH_TO_BUY_LUA, Arrays.asList(good, activity), Arrays.asList(uid))).intValue();
            }
        } finally { 
            if (jedis != null) { 
                jedis.close();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException { 
        final String scriptsha1 = rushToBuySHA1();
        final String goodid = "good0";
        final String activityid = "active1";
        reset(goodid, activityid, 10);

        Map<String, Integer> map = new ConcurrentHashMap<>();
        ExecutorService service = Executors.newCachedThreadPool();
        List<Callable<Integer>> tasks = new ArrayList<>();
        for (int i = 0; i < 50000; i++) { 
            final String uid = "uid" + i;
            tasks.add(() -> { 
                int r = rushToBuy(goodid, activityid, "" + uid, scriptsha1);
                if (r == 1) { 
                    map.put(uid, r);
                }
                return r;
            });
        }
        service.invokeAll(tasks);
        System.out.println(map.size());
        System.out.println(map);
        service.shutdownNow();
    }
}

运行main函数,控制台打印

10
{uid6=1, uid7=1, uid4=1, uid5=1, uid2=1, uid3=1, uid0=1, uid1=1, uid8=1, uid10=1}

商品10的库存,50000个请求并发,最后10个用户获取了库存。

    原文作者:姚仔
    原文地址: https://blog.csdn.net/weixin_39660145/article/details/85334457
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞