redis服务器已经成为时下最流行的内存服务器。可以解决分布式系统session一致性问题,实现分布式锁以及数据缓存。接下来演示springboot2.x集成redis。并实现分布式锁。
1.在springboot项目的pom.xml
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.1.7.RELEASE
com.mycode
spring-demo
0.0.1-SNAPSHOT
spring-shiro
demo project for Spring Boot
UTF-8
UTF-8
1.8
true
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-data-redis
org.apache.commons
commons-pool2
2.7.0
com.alibaba
fastjson
1.2.47
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-maven-plugin
2.在application.yml中配置redis参数
spring:
application:
name: demo
redis: #redis config
host: 192.168.194.221
port: 6379
password: xxxxx #默认无此配置
timeout: 10000
lettuce:
pool:
max-active: 8 #连接池最大连接数
min-idle: 0 #连接池中的最小空闲连接
max-idle: 8 #连接池中的最大空闲连接
max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制)
3.redis的配置类
import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @Deacription redis配置类
* @Author wenyt
* @Date 2020/4/8 16:51
* @Version 1.0
**/
@Configuration
public class RedisConfig {
@Autowired
private LettuceConnectionFactory redisConnectionFactory;
@Bean(name = "redisTemplate")
public RedisTemplate, Object> redisTemplate() {
RedisTemplate, Object> template = new RedisTemplate<>();
StringRedisSerializer keySerializer = new StringRedisSerializer();
GenericFastJsonRedisSerializer valueSerializer = new GenericFastJsonRedisSerializer();
template.setConnectionFactory(redisConnectionFactory);
template.setKeySerializer(keySerializer);
template.setValueSerializer(valueSerializer);
template.setHashKeySerializer(keySerializer);
template.setHashValueSerializer(valueSerializer);
return template;
}
@Bean(name = "stringRedisTemplate")
public StringRedisTemplate stringRedisTemplate() {
StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
stringRedisTemplate.setConnectionFactory(redisConnectionFactory);
return stringRedisTemplate;
}
}
通过以上简单的配置,我们就可以在springboot项目使用springboot为我们提供的redisTemplate和stringRedisTemplate组件对redis服务器进程操作了
1.哪些场景需要用分布式锁
场景一:比较敏感的数据比如金额修改,同一时间只能有一个人操作,想象下2个人同时修改金额,一个加金额一个减金额,为了防止同时操作造成数据不一致,需要锁,如果是数据库需要的就是行锁或表锁,如果是在集群里,多个客户端同时修改一个共享的数据就需要分布式锁。
场景二:比如多台机器都可以定时执行某个任务,如果限制任务每次只能被一台机器执行,不能重复执行,就可以用分布式锁来做标记。
场景三:比如秒杀场景,要求并发量很高,那么同一件商品只能被一个用户抢到,那么就可以使用分布式锁实现。
2.分布式锁实现方式:
Redis实现分布式锁利用 SETNX 和 SETEX基本命令主要有:
4.代码实现
全局锁类:
@Data
public class Lock {
/**
* key名
*/
private String name;
/**
* value值
*/
private String value;
public Lock(String name, String value) {
this.name = name;
this.value = value;
}
}
分布式锁类
@Slf4j
@Component
public class DistributedLockConfig {
/**
* 单个业务持有锁的时间30s,防止死锁
*/
private final static long LOCK_EXPIRE = 30 * 1000L;
/**
* 默认30ms尝试一次
*/
private final static long LOCK_TRY_INTERVAL = 30L;
/**
* 默认尝试20s
*/
private final static long LOCK_TRY_TIMEOUT = 20 * 1000L;
private RedisTemplate template;
public void setTemplate(RedisTemplate template) {
this.template = template;
}
/**
* 尝试获取全局锁
*
* @param lock 锁的名称
* @return true 获取成功,false获取失败
*/
public boolean tryLock(Lock lock) {
return getLock(lock, LOCK_TRY_TIMEOUT, LOCK_TRY_INTERVAL, LOCK_EXPIRE);
}
/**
* 尝试获取全局锁
* SETEX:可以设置超时时间
*
* @param lock 锁的名称
* @param timeout 获取超时时间 单位ms
* @return true 获取成功,false获取失败
*/
public boolean tryLock(Lock lock, long timeout) {
return getLock(lock, timeout, LOCK_TRY_INTERVAL, LOCK_EXPIRE);
}
/**
* 尝试获取全局锁
*
* @param lock 锁的名称
* @param timeout 获取锁的超时时间
* @param tryInterval 多少毫秒尝试获取一次
* @return true 获取成功,false获取失败
*/
public boolean tryLock(Lock lock, long timeout, long tryInterval) {
return getLock(lock, timeout, tryInterval, LOCK_EXPIRE);
}
/**
* 尝试获取全局锁
*
* @param lock 锁的名称
* @param timeout 获取锁的超时时间
* @param tryInterval 多少毫秒尝试获取一次
* @param lockExpireTime 锁的过期
* @return true 获取成功,false获取失败
*/
public boolean tryLock(Lock lock, long timeout, long tryInterval, long lockExpireTime) {
return getLock(lock, timeout, tryInterval, lockExpireTime);
}
/**
* 操作redis获取全局锁
*
* @param lock 锁的名称
* @param timeout 获取的超时时间
* @param tryInterval 多少ms尝试一次
* @param lockExpireTime 获取成功后锁的过期时间
* @return true 获取成功,false获取失败
*/
public boolean getLock(Lock lock, long timeout, long tryInterval, long lockExpireTime) {
try {
if (StringUtils.isEmpty(lock.getName()) || StringUtils.isEmpty(lock.getValue())) {
return false;
}
long startTime = System.currentTimeMillis();
do {
if (!template.hasKey(lock.getName())) {
ValueOperations<String, String> ops = template.opsForValue();
ops.set(lock.getName(), lock.getValue(), lockExpireTime, TimeUnit.MILLISECONDS);
return true;
} else {
//存在锁
log.debug("lock is exist!!!");
}
//尝试超过了设定值之后直接跳出循环
if (System.currentTimeMillis() - startTime > timeout) {
return false;
}
//每隔多长时间尝试获取
Thread.sleep(tryInterval);
}
while (template.hasKey(lock.getName()));
} catch (InterruptedException e) {
log.error(e.getMessage());
return false;
}
return false;
}
/**
* 获取锁
* SETNX(SET If Not Exists):当且仅当 Key 不存在时,则可以设置,否则不做任何动作。
*/
public Boolean getLockNoTime(Lock lock) {
if (!StringUtils.isEmpty(lock.getName())) {
return false;
}
// setIfAbsent 底层封装命令 是 setNX()
boolean falg = template.opsForValue().setIfAbsent(lock.getName(), lock.getValue());
return false;
}
/**
* 释放锁
*/
public void releaseLock(Lock lock) {
if (!StringUtils.isEmpty(lock.getName())) {
template.delete(lock.getName());
}
}
}
测试方法
@RequestMapping("test")
public String index() {
distributedLockConfig.setTemplate(redisTemplate);
Lock lock = new Lock("test", "test");
if (distributedLockConfig.tryLock(lock)) {
try {
//为了演示锁的效果,这里睡眠5000毫秒
System.out.println("执行方法");
Thread.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
}
distributedLockConfig.releaseLock(lock);
}
return "hello world!";
}
开启两个浏览器窗口,执行方法,我们可以看到两个浏览器在等待执行,当一个返回 hello world! 之后,如果没超时执行另一个也会返回hello world! 两个方法彼此先后返回,说明分布式锁执行成功。
但是存在一个问题:
这段方法是先去查询key是否存在redis中,如果存在走循环,然后根据间隔时间去等待尝试获取,如果不存在则进行获取锁,如果等待时间超过超时时间返回false。
如果在集群环境下也会存在问题假如在哨兵模式中 主节点获取到锁之后,数据没有同步到从节点主节点挂掉了,这样数据完整性不能保证,另一个客户端请求过来,就会一把锁被两个客户端持有,会导致数据一致性出问题。
对此Redis中还提供了另外一种实现分布式锁的方法 Redlock
5.利用 Redlock
Redlock是redis官方提出的实现分布式锁管理器的算法。这个算法会比一般的普通方法更加安全可靠。
为什么选择红锁? 在集群中需要半数以上的节点同意才能获得锁,保证了数据的完整性,不会因为主节点数据存在,主节点挂了之后没有同步到从节点,导致数据丢失。
对于Redis集群模式尽量采用这种分布式锁,保证高可用,数据一致性,就使用Redlock 分布式锁。
pom.xml 增加依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.7.0</version>
</dependency>
获取锁后需要处理的逻辑
/**
* 获取锁后需要处理的逻辑
*/
public interface AquiredLockWorker<T> {
T invokeAfterLockAquire() throws Exception;
}
获取锁管理类
/**
* 获取锁管理类
*/
public interface DistributedLocker {
/**
* 获取锁
* @param resourceName 锁的名称
* @param worker 获取锁后的处理类
* @param
* @return 处理完具体的业务逻辑要返回的数据
* @throws UnableToAquireLockException
* @throws Exception
*/
<T> T lock(String resourceName, AquiredLockWorker<T> worker) throws UnableToAquireLockException, Exception;
<T> T lock(String resourceName, AquiredLockWorker<T> worker, int lockTime) throws UnableToAquireLockException, Exception;
}
异常类
/**
* 异常类
*/
public class UnableToAquireLockException extends RuntimeException {
public UnableToAquireLockException() {
}
public UnableToAquireLockException(String message) {
super(message);
}
public UnableToAquireLockException(String message, Throwable cause) {
super(message, cause);
}
}
获取RedissonClient连接类
/**
* 获取RedissonClient连接类
*/
@Component
public class RedissonConnector {
RedissonClient redisson;
@PostConstruct
public void init(){
redisson = Redisson.create();
}
public RedissonClient getClient(){
return redisson;
}
}
分布式锁实现
@Component
public class RedisLocker implements DistributedLocker{
private final static String LOCKER_PREFIX = "lock:";
@Autowired
RedissonConnector redissonConnector;
@Override
public <T> T lock(String resourceName, AquiredLockWorker<T> worker) throws InterruptedException, UnableToAquireLockException, Exception {
return lock(resourceName, worker, 100);
}
@Override
public <T> T lock(String resourceName, AquiredLockWorker<T> worker, int lockTime) throws UnableToAquireLockException, Exception {
RedissonClient redisson= redissonConnector.getClient();
RLock lock = redisson.getLock(LOCKER_PREFIX + resourceName);
// Wait for 100 seconds seconds and automatically unlock it after lockTime seconds
boolean success = lock.tryLock(100, lockTime, TimeUnit.SECONDS);
if (success) {
try {
return worker.invokeAfterLockAquire();
} finally {
lock.unlock();
}
}
throw new UnableToAquireLockException();
}
}
测试方法
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
for (int i = 0; i < 50; i++) {
scheduledExecutorService.execute(new Worker());
}
scheduledExecutorService.shutdown();
//任务
class Worker implements Runnable {
public Worker() {
}
@Override
public void run() {
try {
redisLocker.lock("tizz1100", new AquiredLockWorker<Object>() {
@Override
public Object invokeAfterLockAquire() {
doTask();
return null;
}
});
} catch (Exception e) {
}
}
void doTask() {
System.out.println(Thread.currentThread().getName() + " ---------- " + LocalDateTime.now());
System.out.println(Thread.currentThread().getName() + " start");
Random random = new Random();
int _int = random.nextInt(200);
System.out.println(Thread.currentThread().getName() + " sleep " + _int + "millis");
try {
Thread.sleep(_int);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " end");
}
}