首先
RedisLockUtils工具类
package com.example.demo.utils;
import org.junit.platform.commons.util.StringUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Component
public class RedisLockUtils {
@Resource
private RedisTemplate redisTemplate;
private static Map<String, LockInfo> lockInfoMap = new ConcurrentHashMap<>();
private static final Long SUCCESS = 1L;
public static class LockInfo {
private String key;
private String value;
private int expireTime;
//更新时间
private long renewalTime;
//更新间隔
private long renewalInterval;
public static LockInfo getLockInfo(String key, String value, int expireTime) {
LockInfo lockInfo = new LockInfo();
lockInfo.setKey(key);
lockInfo.setValue(value);
lockInfo.setExpireTime(expireTime);
lockInfo.setRenewalTime(System.currentTimeMillis());
lockInfo.setRenewalInterval(expireTime * 2000 / 3);
return lockInfo;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public int getExpireTime() {
return expireTime;
}
public void setExpireTime(int expireTime) {
this.expireTime = expireTime;
}
public long getRenewalTime() {
return renewalTime;
}
public void setRenewalTime(long renewalTime) {
this.renewalTime = renewalTime;
}
public long getRenewalInterval() {
return renewalInterval;
}
public void setRenewalInterval(long renewalInterval) {
this.renewalInterval = renewalInterval;
}
}
/**
* 使用lua脚本更新redis锁的过期时间
* @param lockKey
* @param value
* @return 成功返回true, 失败返回false
*/
public boolean renewal(String lockKey, String value, int expireTime) {
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end";
DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>();
redisScript.setResultType(Boolean.class);
redisScript.setScriptText(luaScript);
List<String> keys = new ArrayList<>();
keys.add(lockKey);
Object result = redisTemplate.execute(redisScript, keys, value, expireTime);
System.out.println("更新redis锁的过期时间:{}"+result);
return (boolean) result;
}
/**
* @param lockKey 锁
* @param value 身份标识(保证锁不会被其他人释放)
* @param expireTime 锁的过期时间(单位:秒)
* @return 成功返回true, 失败返回false
*/
public boolean lock(String lockKey, String value, long expireTime) {
return redisTemplate.opsForValue().setIfAbsent(lockKey, value, expireTime, TimeUnit.SECONDS);
}
/**
* redisTemplate解锁
* @param key
* @param value
* @return 成功返回true, 失败返回false
*/
public boolean unlock2(String key, String value) {
Object currentValue = redisTemplate.opsForValue().get(key);
boolean result = false;
if (StringUtils.isNotBlank(String.valueOf(currentValue)) && currentValue.equals(value)) {
result = redisTemplate.opsForValue().getOperations().delete(key);
}
return result;
}
/**
* 定时去检查redis锁的过期时间
*/
@Scheduled(fixedRate = 5000L)
@Async("redisExecutor")
public void renewal() {
long now = System.currentTimeMillis();
for (Map.Entry<String, LockInfo> lockInfoEntry : lockInfoMap.entrySet()) {
LockInfo lockInfo = lockInfoEntry.getValue();
if (lockInfo.getRenewalTime() + lockInfo.getRenewalInterval() < now) {
renewal(lockInfo.getKey(), lockInfo.getValue(), lockInfo.getExpireTime());
lockInfo.setRenewalTime(now);
}
}
}
/**
* 分布式锁设置单独线程池
* @return
*/
@Bean("redisExecutor")
public Executor redisExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(1);
executor.setMaxPoolSize(1);
executor.setQueueCapacity(1);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("redis-renewal-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());
return executor;
}
}
完整的
Controller
package com.example.demo.controller;
import com.example.demo.utils.RedisLockUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
private String goodNumKey = "num";
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private RedisLockUtils redisLock;
/**
* 设置商品库存
* @param num 库存数量
* @return
*/
@GetMapping("/set-num/{num}")
public int setNum(@PathVariable int num) {
redisTemplate.opsForValue().set(goodNumKey, num);
return num;
}
/**
* 获取商品库存
* @return
*/
@GetMapping("/get-num")
public int getNum() {
Object objNum = redisTemplate.opsForValue().get(goodNumKey);
int num = Integer.parseInt((String) objNum);
return num;
}
/**
* 用户带着id来秒杀商品
* @param id 用户id
* @return
*/
@GetMapping("/user/{id}")
public String getUser(@PathVariable String id) {
String key = "user:" + id;
String productId = "product001";
String requestId = productId + Thread.currentThread().getId();
boolean locked = redisLock.lock(productId, requestId, 10);
//如果存在直接返回结果
if (redisTemplate.hasKey(key)) {
return (String) redisTemplate.opsForValue().get(key);
}
//如果有锁重试
if (!locked) {
return "error";
}
try {
//查询库存
Object objNum = redisTemplate.opsForValue().get(goodNumKey);
int num = Integer.parseInt((String) objNum);
if (num > 0) {
num--;
//保存库存
redisTemplate.opsForValue().set(goodNumKey, num);
//添加抢购成功的信息
redisTemplate.opsForValue().set(key, 1);
System.out.println(key + "成功");
return (String) redisTemplate.opsForValue().get(key);
} else {
//添加抢购失败的信息
System.out.println(key + "失败");
// redisTemplate.opsForValue().set(key, 0);
return "0";
}
} finally {
redisLock.unlock2(productId, requestId);
}
}
// 其他接口方法...
}
完整pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.11</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>spring-boot-redis</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-redis</name>
<description>spring-boot-redis</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-commons</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
以上是完整服务端 git地址 spting-boot-redis: springBoot中使用redis实现分布式锁实例demo
下面来写一个程序,多线程异步去模拟大量同时的商品抢购请求 看一下抢购成功的用户数量和库存情况
package maomi.com;
import maomi.com.tools.RedisDistributedLock;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import java.io.IOException;
import java.util.UUID;
public class ConcurrentHttpRequestTest implements Runnable {
/**
* 线程id
*/
public int i;
/**
* 线程名称
*/
public String name;
public ConcurrentHttpRequestTest(int i) {
this.i = i;
this.name = String.format("线程[%s]", i);
}
public void run() {
// 执行线程操作
dosom();
}
public static void main(String[] args) {
//开启500个线程去抢购这个商品
for (int i = 1; i < 500; i++) {
new Thread(new ConcurrentHttpRequestTest(i)).start();
}
}
/**
* 一个线程模拟30次抢购 带着随机用户id
* @return
*/
public boolean dosom() {
for (int j = 0; j <30 ; j++) {
CloseableHttpClient httpClient = HttpClients.createDefault();
String url = "http://127.0.0.1:8080/user/" + UUID.randomUUID();
System.out.println(name + ":" + url);
HttpGet httpGet = new HttpGet(url);
try {
CloseableHttpResponse response = httpClient.execute(httpGet);
System.out.println(name+"-Request: " + response.getEntity().toString());
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
}
下面我们来测试
1.首先设置200个库存
2.然后我们来模拟抢购
我们来看打印 一共服务端收到了2232个请求
成功数量只有200个
看下redis成功写入用户和库存 成功写入用户id为200 库存为0
下面我们去掉分布式锁
来同样设置200库存模拟一下 发现库存为0 但是抢购成功的有6000多个用户
大家猜一下为什么会这样