▌前提摘要
本文基于redis数据库
-- redis 192.168.1.211:6379
redis安装:https://blog.csdn.net/qq_37936542/article/details/78522728
springboot集成redis:https://blog.csdn.net/qq_37936542/article/details/80104308
▌准备工作
导入依赖
org.springframework.boot
spring-boot-starter-data-redis
redis.clients
jedis
application.yml配置redis
spring:
redis:
database: 0
host: 192.168.1.211
port: 6379
password: 123
timeout: 3000
jedis:
pool:
max-active: 8
max-idle: 500
min-idle: 0
配置JedisPool
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
@Configuration
public class JedisConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.jedis.pool.max-active}")
private int maxTotal;
@Value("${spring.redis.jedis.pool.max-idle}")
private int maxIdle;
@Value("${spring.redis.jedis.pool.min-idle}")
private int minIdle;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.timeout}")
private int timeout;
@Bean
public JedisPool redisPoolFactory() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(maxTotal);
jedisPoolConfig.setMaxIdle(maxIdle);
jedisPoolConfig.setMinIdle(minIdle);
JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, password);
return jedisPool;
}
}
▌ redis实现分布式锁流程图
1:redis通过while(true)循环尝试去拿锁,只要拿锁失败继续尝试
2:等到拿锁成功以后,去执行相关的业务逻辑代码
3:业务逻辑执行完毕,最后将锁给释放
▌ redis实现分布式锁的注意事项
1:为了防止程序崩溃导致死锁产生,生成key的时候需要设置合理的过期时间,就算程序崩溃了也可以自动释放锁
2:解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了,通过判断value值来实现
▌ 代码实现
InterfaceLock:定义获取锁&释放锁的方法接口
import redis.clients.jedis.Jedis;
public interface InterfaceLock {
// 获取锁
public void lock(Jedis jedis);
// 释放锁
public void unLock(Jedis jedis);
}
DistributeLock:实现分布式锁
import java.util.Collections;
import java.util.UUID;
import redis.clients.jedis.Jedis;
public class DistributeLock implements InterfaceLock {
// 定义锁的key值
private String key = "lockKey";
// 定义锁key的自动过期时间
private long expireTime = 10000;
// 定义value
private String identity = UUID.randomUUID().toString();
/**
* 加锁操作
*/
public void lock(Jedis jedis) {
// 持续去尝试拿锁
while (true) {
// 一旦拿到锁,结束循环
if (tryLock(jedis))
return;
}
}
/**
* 尝试加锁
*/
public boolean tryLock(Jedis jedis) {
// set方法加锁
String result = jedis.set(key, identity, "NX", "PX", expireTime);
// 加锁成功返回true,反之返回false
if ("OK".equals(result))
return true;
return false;
}
/**
* 释放锁,redis+lua实现
*/
public void unLock(Jedis jedis) {
// lua脚本语义:根据传入的key获取value,如果value等于传入的identity则删除key
// 保证了加锁和解锁是同一个客户端
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 执行lua脚本
jedis.eval(script, Collections.singletonList(key), Collections.singletonList(identity));
}
}
jedis.set()方法详解:
key:定义的锁key
value:随机生成的uuid,用于判断加锁和解锁操作是否是同一个客户端
nxxx:只能设置NX或者XX,如果设置NX,则只有当key不存在是才进行set,如果取XX,则只有当key已经存在时才进行set
expx:只能设置EX或者PX,代表数据过期时间的单位,EX代表秒,PX代表毫秒
time:key过期时间
▌ 案例图解
不加锁的抢购代码:SedKillController(两个服务器的代码一致)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import com.mote.service.RedisService;
@RestController
public class SedKillController {
@Autowired
private RedisService redisService;
// 定义商品key值
private String key = "goods";
@GetMapping("sedkill")
public String sedKill() {
// 获取商品数量
Object obj = redisService.get(key);
int mount = (int) obj;
// 如果商品被抢完,直接返回
if (mount < 0 || mount == 0) {
return "很遗憾,商品已被抢完";
}
// 线程睡眠,目的在于放大错误
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 抢到商品后,将redis的商品数量减一
redisService.set(key, --mount);
// 打印,以便观察
System.out.println(Thread.currentThread().getName() + ":抢到第" + (mount + 1) + "件商品");
return "恭喜,商品抢购成功";
}
}
首先往redis放10件商品
启动8080、8081服务,打开浏览器请求两个服务器的接口进行测试,观察打印结果
8080打印:
8081打印:
完犊子,这都是些啥,下面我们使用常用的synchronized锁试试效果,修改sedkill方法
在重新测试,查看打印结果,不要忘记将redis的商品数据重置为10奥
8080打印:
8081打印:
咦,有惊喜,比之前打印结果好看多了,至少单服务器不会出现抢购同一商品的情况了,但是还是有问题,两台服务器竟然可以抢到同一件商品,这是绝对不允许的,下面我们尝试加上刚写好的分布式锁尝试一下
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import com.mote.lock.DistributeLock;
import com.mote.service.RedisService;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
@RestController
public class SedKillController {
@Autowired
private RedisService redisService;
private String key = "goods";
@Autowired
private JedisPool jedisPool;
@GetMapping("sedkill")
public String sedKill() {
// 获取分布式锁对象
DistributeLock lock = new DistributeLock();
// 获取jedis
Jedis jedis = jedisPool.getResource();
try {
// 上锁
lock.lock(jedis);
// 获取商品数量
Object obj = redisService.get(key);
int mount = (int) obj;
// 如果商品被抢完,直接返回
if (mount < 0 || mount == 0) {
// 解锁(不要忘了这里解锁)
lock.unLock(jedis);
return "很遗憾,商品已被抢完";
}
// 线程睡眠,目的在于放大错误
Thread.sleep(2000);
// 抢到商品后,将redis的商品数量减一
redisService.set(key, --mount);
// 打印,以便观察
System.out.println(Thread.currentThread().getName() + ":抢到第" + (mount + 1) + "件商品");
// 解锁
lock.unLock(jedis);
return "恭喜,商品抢购成功";
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 关闭jedis连接
jedis.close();
}
return "很遗憾,商品已被抢完";
}
}
再走一遍,开启服务器,访问接口,查看打印,不要忘记将redis商品重新置为10
8080打印:
8081打印:
完美打印,符合预期