使用Redis和Lua的原子性实现抢红包功能

    技术2022-07-10  148

    使用Redis和Lua的原子性实现抢红包功能

    安装Lua(可选)编写lua脚本lua脚本学习可以参考 [https://www.runoob.com/lua/lua-basic-syntax.html](https://www.runoob.com/lua/lua-basic-syntax.html). 使用 Redis 实现抢红包测试 总结

    安装Lua(可选)

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

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

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

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

    编写lua脚本

    lua脚本学习可以参考 https://www.runoob.com/lua/lua-basic-syntax.html.
    --缓存抢红包列表信息列表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中,很好的解决了高并发的问题。

    Processed: 0.017, SQL: 9