使用Redis和Lua的原子性实现抢红包功能
安装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中,很好的解决了高并发的问题。
最后
关注我公众号,专注分享Java技术干货,包括多线程、JVM、Spring Boot、Spring CloudRedis、架构设计、微服务、消息队列、Linux、面试题、程序员攻略、最新动态等。