首先
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 {@Resourceprivate 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";@Autowiredprivate RedisTemplate redisTemplate;@Autowiredprivate 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多个用户
大家猜一下为什么会这样