目标:
巧用 Redis => set 命令 解决订单超卖场景
Redis具有极高的性能,且其命令对分布式锁支持友好,借助SET命令即可实现加锁处理。
步骤:
1、启动Redis环境2、搭建demo,消费同一个redis的key
3、使用 setIfAbsent 功能
4、使用Apifox并发消费,看结果
5、升级再升级!
Tips:其余步骤可看上篇博客
二:搭建demo,消费同一个redis的key
application.properties
# 应用名称
spring.application.name=redis_03
# 端口号
server.port=8080
#=========================redis相关配香========================
#Redis数据库索引(默认方0)
spring.redis.database=0
#Redis服务器地址
spring.redis.host=192.168.128.128
#Redis服务器连接端口
spring.redis.port=6379
#Redis服务器连接密码(默认为空)
spring.redis.password=
#连接池最大连接数(使用负值表示没有限制)默认8
spring.redis.lettuce.pool.max-active=8
#连接池最大阻塞等待时间(使用负值表示没有限制)默认-1
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接默认8
spring.redis.lettuce.pool.max-idle=8
#连接池中的最小空闲连接默犬认0
spring.redis.lettuce.pool.min-idle=0
三:使用 setIfAbsent 功能
使用Redis中String类型的方法 setIfAbsent
,判断当前是否有LOCK这个key,没有就存入并返回true,有就不存并返回false,下面执行业务代码做判断即可
GoodsController
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
@RestController
public class GoodsController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private final String LOCK = "REDIS_LOCK";
@GetMapping("/buyBy")
public String buyBy01(){
String uuid = UUID.randomUUID().toString();
try {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(LOCK, uuid);
if(flag == false){
return "无权限执行...";
}
// 查看redis中库存数量
String res = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNum = res == null ? 0 : Integer.parseInt(res);
if(goodsNum > 0 ){
int realNum = goodsNum - 1;
stringRedisTemplate.opsForValue().set("goods:001",String.valueOf(realNum));
System.out.println("buyBy01:成功购买商品,库存仅剩:"+realNum);
return "buyBy01:成功购买商品,库存仅剩:"+realNum;
}else{
System.out.println("buyBy01:商品售罄");
}
}finally {
stringRedisTemplate.delete(LOCK);
}
return "buyBy01:商品售罄";
}
@GetMapping("/buyBy2")
public String buyBy02(){
String uuid = UUID.randomUUID().toString();
try {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(LOCK, uuid);
if(flag == false){
return "无权限执行...";
}
// 查看redis中库存数量
String res = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNum = res == null ? 0 : Integer.parseInt(res);
if(goodsNum > 0 ){
int realNum = goodsNum - 1;
stringRedisTemplate.opsForValue().set("goods:001",String.valueOf(realNum));
System.out.println("buyBy02:成功购买商品,库存仅剩:"+realNum);
return "buyBy02:成功购买商品,库存仅剩:"+realNum;
}else{
System.out.println("buyBy02:商品售罄");
}
}finally {
stringRedisTemplate.delete(LOCK);
}
return "buyBy02:商品售罄";
}
}
四:使用Apifox并发消费,看结果
结果如下:
五:升级再升级!
情况一:分布式情况下一台机器崩了,假设刚好走完try到代码块,没走到finally 去释放锁,咋办?
可以使用redis中的string类型的expire设置过期key方式,但是如果这样子语句就没有原子性了…
实现代码如下:
setIfAbsent(LOCK, uuid,10L, TimeUnit.SECONDS)
@RestController
public class GoodsController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private final String LOCK = "REDIS_LOCK";
@GetMapping("/buyBy")
public String buyBy01(){
String uuid = UUID.randomUUID().toString();
try {
// 设置key+过期时间分开了,必须要合并成一行具备原子性
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(LOCK, uuid,10L, TimeUnit.SECONDS);
if(flag == false){
return "无权限执行...";
}
// 查看redis中库存数量
String res = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNum = res == null ? 0 : Integer.parseInt(res);
if(goodsNum > 0 ){
int realNum = goodsNum - 1;
stringRedisTemplate.opsForValue().set("goods:001",String.valueOf(realNum));
System.out.println("buyBy01:成功购买商品,库存仅剩:"+realNum);
return "buyBy01:成功购买商品,库存仅剩:"+realNum;
}else{
System.out.println("buyBy01:商品售罄");
}
}finally {
stringRedisTemplate.delete(LOCK);
}
return "buyBy01:商品售罄";
}
@GetMapping("/buyBy2")
public String buyBy02(){
String uuid = UUID.randomUUID().toString();
try {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(LOCK, uuid,10L,TimeUnit.SECONDS);
if(flag == false){
return "无权限执行...";
}
// 查看redis中库存数量
String res = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNum = res == null ? 0 : Integer.parseInt(res);
if(goodsNum > 0 ){
int realNum = goodsNum - 1;
stringRedisTemplate.opsForValue().set("goods:001",String.valueOf(realNum));
System.out.println("buyBy02:成功购买商品,库存仅剩:"+realNum);
return "buyBy02:成功购买商品,库存仅剩:"+realNum;
}else{
System.out.println("buyBy02:商品售罄");
}
}finally {
stringRedisTemplate.delete(LOCK);
}
return "buyBy02:商品售罄";
}
}
情况二:在执行业务的过程中,由于中间某个过程比较慢,导致分布式锁过期了,而别的线程进来了,当这个线程执行完的时候,会删除分布式锁,但是却把别的线程的锁给删掉了,咋办?
这好办,在释放锁时判断当前锁的value是否一致即可
// 在释放锁时判断当前锁的value是否一致
if(stringRedisTemplate.opsForValue().get(LOCK).equals(uuid)){
stringRedisTemplate.delete(LOCK);
}
@RestController
public class GoodsController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private final String LOCK = "REDIS_LOCK";
@GetMapping("/buyBy")
public String buyBy01(){
String uuid = UUID.randomUUID().toString();
try {
// 设置key+过期时间分开了,必须要合并成一行具备原子性
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(LOCK, uuid,10L, TimeUnit.SECONDS);
if(flag == false){
return "无权限执行...";
}
// 查看redis中库存数量
String res = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNum = res == null ? 0 : Integer.parseInt(res);
if(goodsNum > 0 ){
int realNum = goodsNum - 1;
stringRedisTemplate.opsForValue().set("goods:001",String.valueOf(realNum));
System.out.println("buyBy01:成功购买商品,库存仅剩:"+realNum);
return "buyBy01:成功购买商品,库存仅剩:"+realNum;
}else{
System.out.println("buyBy01:商品售罄");
}
}finally {
// 在释放锁时判断当前锁的value是否一致
if(stringRedisTemplate.opsForValue().get(LOCK).equals(uuid)){
stringRedisTemplate.delete(LOCK);
}
}
return "buyBy01:商品售罄";
}
@GetMapping("/buyBy2")
public String buyBy02(){
String uuid = UUID.randomUUID().toString();
try {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(LOCK, uuid,10L,TimeUnit.SECONDS);
if(flag == false){
return "无权限执行...";
}
// 查看redis中库存数量
String res = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNum = res == null ? 0 : Integer.parseInt(res);
if(goodsNum > 0 ){
int realNum = goodsNum - 1;
stringRedisTemplate.opsForValue().set("goods:001",String.valueOf(realNum));
System.out.println("buyBy02:成功购买商品,库存仅剩:"+realNum);
return "buyBy02:成功购买商品,库存仅剩:"+realNum;
}else{
System.out.println("buyBy02:商品售罄");
}
}finally {
// 在释放锁时判断当前锁的value是否一致
if(stringRedisTemplate.opsForValue().get(LOCK).equals(uuid)){
stringRedisTemplate.delete(LOCK);
}
}
return "buyBy02:商品售罄";
}
}
情况三:finally块的判断 + del删除操作不是原子性的
那 redis事务安排上咯
Redis的事条是通过MULTI,EXEC,DISCARD和WATCH这四个命令来完成。
Redis的单个命令都是原子性的,所以这里确保事务性的对象是命令集合。
Redis将命令集合序列化并确保处于一事务的命令集合连续且不被打断的执行。
while(true){
stringRedisTemplate.watch(LOCK);
// 在释放锁时判断当前锁的value是否一致
if(stringRedisTemplate.opsForValue().get(LOCK).equalsIgnoreCase(uuid)){
stringRedisTemplate.setEnableTransactionSupport(true);
stringRedisTemplate.multi();
stringRedisTemplate.delete(LOCK);
List<Object> list = stringRedisTemplate.exec();
if (list == null) {
continue;
}
}
stringRedisTemplate.unwatch();
break;
}
@RestController
public class GoodsController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private final String LOCK = "REDIS_LOCK";
@GetMapping("/buyBy")
public String buyBy01(){
String uuid = UUID.randomUUID().toString();
try {
// 设置key+过期时间分开了,必须要合并成一行具备原子性
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(LOCK, uuid,10L, TimeUnit.SECONDS);
if(flag == false){
return "无权限执行...";
}
// 查看redis中库存数量
String res = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNum = res == null ? 0 : Integer.parseInt(res);
if(goodsNum > 0 ){
int realNum = goodsNum - 1;
stringRedisTemplate.opsForValue().set("goods:001",String.valueOf(realNum));
System.out.println("buyBy01:成功购买商品,库存仅剩:"+realNum);
return "buyBy01:成功购买商品,库存仅剩:"+realNum;
}else{
System.out.println("buyBy01:商品售罄");
}
}finally {
while(true){
stringRedisTemplate.watch(LOCK);
// 在释放锁时判断当前锁的value是否一致
if(stringRedisTemplate.opsForValue().get(LOCK).equalsIgnoreCase(uuid)){
stringRedisTemplate.setEnableTransactionSupport(true);
stringRedisTemplate.multi();
stringRedisTemplate.delete(LOCK);
List<Object> list = stringRedisTemplate.exec();
if (list == null) {
continue;
}
}
stringRedisTemplate.unwatch();
break;
}
}
return "buyBy01:商品售罄";
}
@GetMapping("/buyBy2")
public String buyBy02(){
String uuid = UUID.randomUUID().toString();
try {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(LOCK, uuid,10L,TimeUnit.SECONDS);
if(flag == false){
return "无权限执行...";
}
// 查看redis中库存数量
String res = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNum = res == null ? 0 : Integer.parseInt(res);
if(goodsNum > 0 ){
int realNum = goodsNum - 1;
stringRedisTemplate.opsForValue().set("goods:001",String.valueOf(realNum));
System.out.println("buyBy02:成功购买商品,库存仅剩:"+realNum);
return "buyBy02:成功购买商品,库存仅剩:"+realNum;
}else{
System.out.println("buyBy02:商品售罄");
}
}finally {
while(true){
stringRedisTemplate.watch(LOCK);
// 在释放锁时判断当前锁的value是否一致
if(stringRedisTemplate.opsForValue().get(LOCK).equalsIgnoreCase(uuid)){
stringRedisTemplate.setEnableTransactionSupport(true);
stringRedisTemplate.multi();
stringRedisTemplate.delete(LOCK);
List<Object> list = stringRedisTemplate.exec();
if (list == null) {
continue;
}
}
stringRedisTemplate.unwatch();
break;
}
}
return "buyBy02:商品售罄";
}
}
情况四:如果是在集群环境下,我们自己实现其实并没有RedLock做得好
RedLock之Redisson落地
Redisson配置类
@Configuration
public class RedisConfig {
@Bean
public Redisson redisson(){
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.128.128").setDatabase(0);
return (Redisson) Redisson.create(config);
}
}
请求接口
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GoodsController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
@Autowired
private Redisson redisson;
public static final String REDIS_LOCK = "REDIS_LOCK";
@GetMapping("/bugOne")
public String buySomething(){
RLock rLock = redisson.getLock(REDIS_LOCK);
rLock.lock();
try {
// 查看redis中库存数量
String res = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNum = res == null ? 0 : Integer.parseInt(res);
if(goodsNum > 0 ){
int realNum = goodsNum - 1;
stringRedisTemplate.opsForValue().set("goods:001",String.valueOf(realNum));
System.out.println("buySomething:成功购买商品,库存仅剩:"+realNum);
return "buySomething:成功购买商品,库存仅剩:"+realNum;
}else{
System.out.println("buySomething:商品售罄");
}
}finally {
rLock.unlock();
}
return "buySomething:商品售罄";
}
@GetMapping("/buyTwo")
public String buyAnything(){
RLock rLock = redisson.getLock(REDIS_LOCK);
rLock.lock();
try {
// 查看redis中库存数量
String res = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNum = res == null ? 0 : Integer.parseInt(res);
if(goodsNum > 0 ){
int realNum = goodsNum - 1;
stringRedisTemplate.opsForValue().set("goods:001",String.valueOf(realNum));
System.out.println("buyAnything:成功购买商品,库存仅剩:"+realNum);
return "buyAnything:成功购买商品,库存仅剩:"+realNum;
}else{
System.out.println("buyAnything:商品售罄");
}
}finally{
rLock.unlock();
}
return "buyAnything:商品售罄";
}
}
最终保险版本
if(rLock.isLocked() && rLock.isHeldByCurrentThread()){
rLock.unlock();
}
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GoodsController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
@Autowired
private Redisson redisson;
public static final String REDIS_LOCK = "REDIS_LOCK";
@GetMapping("/bugOne")
public String buySomething(){
RLock rLock = redisson.getLock(REDIS_LOCK);
rLock.lock();
try {
// 查看redis中库存数量
String res = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNum = res == null ? 0 : Integer.parseInt(res);
if(goodsNum > 0 ){
int realNum = goodsNum - 1;
stringRedisTemplate.opsForValue().set("goods:001",String.valueOf(realNum));
System.out.println("buySomething:成功购买商品,库存仅剩:"+realNum);
return "buySomething:成功购买商品,库存仅剩:"+realNum;
}else{
System.out.println("buySomething:商品售罄");
}
}finally {
if(rLock.isLocked() && rLock.isHeldByCurrentThread()){
rLock.unlock();
}
}
return "buySomething:商品售罄";
}
@GetMapping("/buyTwo")
public String buyAnything(){
RLock rLock = redisson.getLock(REDIS_LOCK);
rLock.lock();
try {
// 查看redis中库存数量
String res = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNum = res == null ? 0 : Integer.parseInt(res);
if(goodsNum > 0 ){
int realNum = goodsNum - 1;
stringRedisTemplate.opsForValue().set("goods:001",String.valueOf(realNum));
System.out.println("buyAnything:成功购买商品,库存仅剩:"+realNum);
return "buyAnything:成功购买商品,库存仅剩:"+realNum;
}else{
System.out.println("buyAnything:商品售罄");
}
}finally{
if(rLock.isLocked() && rLock.isHeldByCurrentThread()){
rLock.unlock();
}
}
return "buyAnything:商品售罄";
}
}
最后总体回顾
-
如果是单机版锁,上
synchronized
或者ReentraLock
-
如果是nginx分布式微服务的话,单机锁则不行了,得上Redis分布式锁
setIfAbsent()
-
只加了锁,没有释放锁,出异常的话,可能无法释放锁,必须要在代码层面
finally释放锁
-
假设宕机了,部署了微服务代码层面根本没有走到finally这块,就没办法保证解锁了,这个key没有被删除,则需要
设置lock的过期时间
(还必须要setnx+过期时间必须同一行
) -
并且必须规定
只能自己删除自己的锁
,你不能把别人的锁删除了
-
-
Redis集群环境下,直接上
RedLock
的Redisson