手把手教你实现微信抢红包功能

261 阅读2分钟

安装Lua(可选)

  1. 参考www.lua.org/ftp/.教程,下载5.3.5_1版本,本地安装,如果你使用的是Mac,那建议用brew工具直接执行brew install lua就可以顺利安装,

  2. 有关brew工具的安装可以参考brew.sh/.网站,建议翻墙否则会很慢。

  3. 安装IDEA插件,在IDEA->Preferences面板,Plugins,里面Browse repositories,在里面搜索lua,然后就选择同名插件lua。安装好后重启IDEA

  4. 配置Lua SDK的位置: IDEA->File->Project Structure,选择添加Lua,路径指向Lua SDK的文件夹

编写lua脚本

lua脚本学习可以参考 www.runoob.com/lua/lua-bas….

--缓存抢红包列表信息列表key
local listKey = 'red_packet_list_'..KEYS[1]
--当前被抢红包key
local redPacket = 'red_packet_'..KEYS[1]
--获取当前红包库存
local stock = tonumber(redis.call('hget', redPacket, 'stock'))
--没有库存,返回为0
if stock <= 0 then return 0 end
--库存减1
stock = stock -1
--保存当前库存
redis.call('hset',redPacket,'stock', tostring(stock))
--往链表中加入当前红包信息
redis.call('rpush', listKey, ARGV[1])
--如果是最后一个红包,则返回2,表示抢红包已经结束,需要将列表中的数据保存到数据库中
if stock == 0 then return 2 end
--如果并非最后一个红包,则返回1,表示抢红包成功
return 1

使用 Redis 实现抢红包

加载lua脚本

@Configuration
public class RedisConfiguration {
    @Bean(name = "redPacket")
    public DefaultRedisScript<Long> loadRedPackRedisScript() {
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setLocation(new ClassPathResource("redPacket.lua"));
        redisScript.setResultType(java.lang.Long.class);
        return redisScript;
    }
}

**UserRedPacketService **

public interface UserRedPacketService {
    /**
     * 通过Redis实现抢红包
     * @param redPacketId 红包编号
     * @param userId 用户编号
     *@return 0-没有库存,失败
     * 1-成功,且不是最后一个红包
     * 2-成功,且是最后一个红包
     */
    Long grapRedPacketByRedis(Long redPacketId, Long userId);
}

**UserRedPacketServiceImpl **

@Slf4j
@Service
public class UserRedPacketServiceImpl implements UserRedPacketService {
    private final RedisScript<Long> ratePacket;
    private final StringRedisTemplate stringRedisTemplate;

    public UserRedPacketServiceImpl(@Qualifier("redPacket") RedisScript<Long> ratePacket, StringRedisTemplate stringRedisTemplate) {
        this.ratePacket = ratePacket;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public Long grapRedPacketByRedis(Long redPacketId, Long userId) {
        // 当前抢红包用户和日期信息
        String args = userId + "_" + System.currentTimeMillis();
        Long result = stringRedisTemplate.execute(ratePacket, Lists.newArrayList(redPacketId + ""), args);
        log.info("返回结果:{}", result);
        return result;
    }
}

UserRedPacketController

  @Autowired
    private UserRedPacketService userRedPacketService;
    @GetMapping("/grapRedPacketByRedis")
    public boolean grapRedPacketByRedis(Long redPacketId, Long userId) {
        Long result = userRedPacketService.grapRedPacketByRedis(redPacketId, userId);
        Map<String, Object> resultMap = Maps.newHashMap();
        boolean flag = result > 0;
        return flag;
    }

测试

红包编号为1和红包数量数量为8个以及每个红包金额为5 hset red_packet_1 stock 8 hset red_packet_1 unit_amount 10

public class RedisPackThread {
    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor redPacketExecutor = new ThreadPoolExecutor(100, 1000, 60L,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(1000), r -> {
            //            t.setName("redPacket");
            return new Thread(r);
        }, (r, executor) -> {
            System.out.println("async sender is error rejected, runnable: " + r + ", executor: {}" + executor);
        });
        CountDownLatch cdl = new CountDownLatch(100);
        CyclicBarrier cyclicBarrier = new CyclicBarrier(100);
        for (int i = 0; i < 100; i++) {
            int finalI = i;
            redPacketExecutor.submit(() -> {
                try {
                    cyclicBarrier.await();
                    RestTemplate restTemplate = new RestTemplate();
                    Boolean result = restTemplate.getForObject("http://localhost:8888/grapRedPacketByRedis?redPacketId=1&userId=" + finalI, Boolean.class);
                    if(Objects.requireNonNull(result))
                    {
                        System.out.println("我是线程:" + finalI + "  我抢到红包了");
                    }
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                } finally {
                    cdl.countDown();
                }
            });
        }
        cdl.await();
        redPacketExecutor.shutdown();
    }
}

运行结果 在这里插入图片描述

总结

redis执行lua脚本的时候,会将它作为一个整体执行,要么全部执行成功,如果出现异常则执行结果不会更新到redis中,很好的解决了高并发的问题。