一段模拟抢红包的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| public class RedisTest {
public Object rob(String userId, String redId) {
Object object = RedisClient.get(redId + ":" + userId + ":rob"); if (null != object) { return object; }
Object total = RedisClient.get(redId + ":total"); int totalNum = Integer.parseInt(total.toString()); if (null != total && totalNum > 0) { Object value = RedisClient.getRightList(redId); if (null != value) { RedisClient.set(redId + ":total", totalNum - 1);
saveAsync(value);
long time = 60 * 60 * 24; RedisClient.set(redId + ":" + userId + ":rob", value, time); return value; } } return null; }
@Async public void saveAsync(Object object) {
}
}
|
以上代码,在单个请求的正常测试下,是没有问题的。但是在实际生产环境中,出现“秒级高并发请求”时,没有控制的多线程会出现抢占资源的情况。从而会出现很多问题。比如数据不一致,或者一个用户抢到了很多个红包。
Redis 底层架构是采用单线程进行设计的,因此它提供的这些操作也是单线程的。操作具有原子性。
原子性 指同一时刻只能有一个线程处理核心业务逻辑。当有其他线程对应的请求过来时,如果前面的线程没有处理完毕,则当前线程将进入等待状态(堵塞),直到前面的线程处理完毕。
优化
通过 Redis 的原子操作 setIfAbsent()方法对该业务逻辑加分布式锁。当多个并发线程同一时刻调用 setIfAbsent()时,Redis 底层是会将线程加入队列处理的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
|
public Object rob2(String userId, String redId) { Object object = RedisClient.get(redId + ":" + userId + ":rob"); if (null != object) { return object; }
Object total = RedisClient.get(redId + ":total"); int totalNum = Integer.parseInt(total.toString()); if (null != total && totalNum > 0) { String lockKey = redId + ":" + userId + ":lock"; long expireTime = 60 * 60 * 24; boolean lock = RedisClient.setIfAbsent(lockKey, redId, expireTime);
try { if (lock) { Object value = RedisClient.getRightList(redId); if (null != value) { RedisClient.set(redId + ":total", totalNum - 1);
saveAsync(value);
long time = 60 * 60 * 24; RedisClient.set(redId + ":" + userId + "rob", value, time); return value; } } } catch (Exception e) { e.printStackTrace(); } } return null; }
|
RedisClient.setIfAbsent
方法
1 2 3 4 5 6
| public static boolean setIfAbsent(final String key, Object value, Long expireTime) { ValueOperations valueOperations = redisTemplate.opsForValue(); Boolean aBoolean = valueOperations.setIfAbsent(key, value); redisTemplate.expire(key, expireTime, TimeUnit.SECONDS); return aBoolean; }
|
此时,在通过 JMeter 每秒 1000 个线程测试,就不会出现一个用户抢到多个红包了。
至此,Redis 分布式锁已经实现。当然,这里只是简单实现了一下。分布式锁可以有很多方式实现,可以再研究其他实现方式。
参考文章:
《分布式中间件技术实战(Java 版)》