1. 事务和锁机制
我们学习redis的乐观锁机制与事务的三大特性
1.1 基本操作
127.0.0.1:6379> set balance 100
OK
# 在shell1中执行watch监听balance
127.0.0.1:6379> watch balance
OK
# 在shell2中执行watch监听balance
127.0.0.1:6379> watch balance
OK
# 在shell1中执行multi开始事务
127.0.0.1:6379> multi
OK
# 在shell2中执行multi开始事务
127.0.0.1:6379> multi
OK
# 在shell1中执行incrby增加余额50
127.0.0.1:6379(TX)> incrby balance 50
QUEUED
# 在shell2中执行incrby减少余额50
127.0.0.1:6379(TX)> decrby balance 50
QUEUED
# 在shell1中执行exec执行事务失败,因为监视到了key被其他命令所改动
127.0.0.1:6379(TX)> exec
(nil)
# 在shell2中执行exec执行事务成功
127.0.0.1:6379(TX)> exec
1) (integer) 50
# unwatch取消对所有key的监视
127.0.0.1:6379> unwatch
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> decrby balance 55
QUEUED
# discard方式事务
127.0.0.1:6379(TX)> discard
OK
1.2 redis事务三大特性
- 单独的隔离操作 事务中的所有命令都会按顺序一一执行。事务执行过程中,不会被其他客户端所打断
- 没有隔离级别的概念 当事务开启时,事务期间的命令并没有执行,而是加入队列,只有执行EXEC命令时,才会执行
- 不保证原子性 事务中如果一条命令执行失败,其他的命令仍会执行,不会回滚
127.0.0.1:6379> set a 100
OK
127.0.0.1:6379> set b 100
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incrby a 50a
QUEUED
# decrby b命令仍会执行,不会回滚
127.0.0.1:6379(TX)> decrby b 50
QUEUED
127.0.0.1:6379(TX)> exec
1) (error) ERR value is not an integer or out of range
2) (integer) 50
127.0.0.1:6379> get b
"50"
127.0.0.1:6379> get a
"100"
2. Redisson分布式锁实现秒杀
本文学习采用redis官方推荐java客户端Redisson分布式锁实现秒杀,从而避免超卖、库存遗留、连接超时问题
所有代码已提交至https://gitee.com/SJshenjian/blog-code/tree/master/src/main/java/online/shenjian/redis
2.1 pom.xml新增依赖配置
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.18.0</version>
</dependency>
</dependencies>
2.2 新增RedissonConfig配置
@Configuration
public class RedissonConfig {
@Bean(name = "stringLongCodec")
public CompositeCodec stringLongCodec() {
return new CompositeCodec(new org.redisson.client.codec.StringCodec(), new org.redisson.client.codec.LongCodec());
}
@Bean(name = "stringCodec")
public StringCodec stringCodec() {
return new org.redisson.client.codec.StringCodec();
}
@Bean("redissonClient")
public RedissonClient redissonClient() {
Config config = new Config();
config.setTransportMode(TransportMode.NIO);
SingleServerConfig singleServerConfig = config.useSingleServer();
singleServerConfig.setAddress("redis://127.0.0.1:6379");
singleServerConfig.setPassword("shenjian.online");
singleServerConfig.setTimeout(2000);
singleServerConfig.setDatabase(0);
singleServerConfig.setConnectionPoolSize(1000);
singleServerConfig.setConnectionMinimumIdleSize(2);
RedissonClient redisson = Redisson.create(config);
return redisson;
}
}
2.3 新增controller与service
@RestController
public class SecKillController {
private SecKillService secKillService;
public SecKillController(SecKillService secKillService) {
this.secKillService = secKillService;
}
@PostMapping(value = "/doSecKill", produces = MediaType.APPLICATION_JSON_VALUE)
public String doSecKill(@RequestParam String userId, @RequestParam String productId) {
return secKillService.doSecKill(userId, productId);
}
}
public interface SecKillService {
String doSecKill(String userId, String productId);
}
2.4 秒杀核心代码逻辑
@Service
@Slf4j
public class SecKillServiceImpl implements SecKillService {
private RedissonClient redissonClient;
// redis中库中信息
private static final String PRODUCT_STOCK_KEY = "sk_10001_stock";
// redis中该商品秒杀到的用户列表
private static final String PRODUCT_USER_KEY = "sk_10001_user";
public SecKillServiceImpl(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}
@Override
public String doSecKill(String userId, String productId) {
RKeys rKeys = redissonClient.getKeys();
if (rKeys.countExists(PRODUCT_STOCK_KEY) == 0) {
return "秒杀未开始";
}
RLock rLock = redissonClient.getLock(PRODUCT_STOCK_KEY + "_lock");
try {
boolean locked = rLock.tryLock(30, 60, TimeUnit.SECONDS);
if (locked) {
RAtomicLong productStock = redissonClient.getAtomicLong(PRODUCT_STOCK_KEY);
RSet<String> productUserSet = redissonClient.getSet(PRODUCT_USER_KEY);
if (productUserSet != null && productUserSet.contains(userId)) {
return "您已秒杀过,请勿重复秒杀";
}
if (productStock.get() <= 0) {
return "很遗憾,秒杀已结束";
}
// 加入到秒杀集合
productUserSet.add(userId);
// 库存减1
productStock.decrementAndGet();
return "恭喜您,秒杀成功!";
}
} catch (InterruptedException e) {
log.error("异常:{}", e.getMessage());
} finally {
rLock.unlock();
}
return "服务器繁忙";
}
}
2.5 jmeter压力测试
SecKill.jmx与userId.csv也已上传到代码resource中,可以直接导入jmeter
- 秒杀未开始
当无sk_10001_stock值时,2000个用户均返回秒杀未开始
- 秒杀
在redis中设置库存数为100
127.0.0.1:6379> set sk_10001_stock 100
再次进行jmeter秒杀
用户数改为1000,错误率为0,吞吐173/sec
秒杀结束
重复秒杀
秒杀成功
继续查看redis,发现库存为0,秒杀到的用户数为100,正确, 到此秒杀实现介绍完毕,下一节我们将介绍redisson分布式锁原理及通过原生lua脚本实现秒杀逻辑
127.0.0.1:6379> get sk_10001_stock
"0"
127.0.0.1:6379> scard sk_10001_user
(integer) 100
欢迎关注公众号算法小生获取最新文章