一.商品减库中存在问题
1.传统的代码
1.1引入jar包
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.6.5</version></dependency><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>2.9.0</version></dependency>
1.2配置application.yml
server:port: 8090spring:redis:host: 192.168.2.66port: 6379
1.3原始测试代码
package com.redisson;import org.redisson.Redisson;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/test")
public class TestDeductController {@Autowiredprivate Redisson redisson;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate RedisTemplate redisTemplate;@RequestMapping("/deduct_stock")public String deductStock() {int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")if (stock > 0) {int realStock = stock - 1;stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)System.out.println("扣减成功,剩余库存:" + realStock);} else {System.out.println("扣减失败,库存不足");}return "end";}
}
1.apache-jmeter测试工具的使用
2.配置nginx代理,修改nginx-1.10.2/conf/nginx.conf,并启动
upstream redislock{server 192.168.2.100:8080 weight=1;server 192.168.2.100:8090 weight=1;}server {listen 80;server_name localhost;#charset koi8-r;#access_log logs/host.access.log main;location / {root html;index index.html index.htm;proxy_pass http://redislock;}}
3创建8080,8090spring boot后端客户端
4.测试结果
说明:当多个客户端同时访问这个减库存的逻辑的时候,会出现多个客户端获取的库存数据是一样的,这样导致库存的实际的扣减值和库存值不一致。
1.4使用synchronized进行代码优化
package com.redisson;import org.redisson.Redisson;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/test")
public class TestDeductController {@Autowiredprivate Redisson redisson;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate RedisTemplate redisTemplate;@RequestMapping("/deduct_stock")public String deductStock() {synchronized (this){int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")if (stock > 0) {int realStock = stock - 1;stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)System.out.println("扣减成功,剩余库存:" + realStock);} else {System.out.println("扣减失败,库存不足");}return "end";}}
}
说明:当存在多台后端服务器时,synchronized也会失效,因为synchronized针对的是单台服务器加锁,出现多台服务器,就不能够很好的实现库存加锁。
1.5设置一个锁,程序解锁后或超时的后删除锁
package com.redisson;import org.redisson.Redisson;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;import java.util.concurrent.TimeUnit;@RestController
@RequestMapping("/test")
public class TestDeductController {@Autowiredprivate Redisson redisson;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate RedisTemplate redisTemplate;@RequestMapping("/deduct_stock")public String deductStock() {String lockKey = "lock:product_101";String clientId = UUID.randomUUID().toString();Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS); //jedis.setnx(k,v)if (!result) {return "error_code";}try {int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")if (stock > 0) {int realStock = stock - 1;stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)System.out.println("扣减成功,剩余库存:" + realStock);} else {System.out.println("扣减失败,库存不足");}return "end";} finally {if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {stringRedisTemplate.delete(lockKey);}}}}
说明:以上情况依然会出现一个问题,当在执行在finally行是,刚刚判断完成,此时的超时的时间到了,此后删除锁不是自身加的锁而是,后面线程加的锁,此后的过程都会删除后面线程的加的锁,依然存在bug
1.6最终优化方案
1.导入jar包
<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.6.5</version></dependency>
2.在启动项中配置redisson
@Beanpublic Redisson redisson() {// 此为单机模式Config config = new Config(); //config.setLockWatchdogTimeout(1000);config.useSingleServer().setAddress("redis://192.168.2.66:6379").setDatabase(0);return (Redisson) Redisson.create(config);}
3.优化代码程序
package com.redisson;import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;import java.util.concurrent.TimeUnit;@RestController
@RequestMapping("/test")
public class TestDeductController {@Autowiredprivate Redisson redisson;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate RedisTemplate redisTemplate;@RequestMapping("/deduct_stock")public String deductStock() {String lockKey = "lock:product_101";//获取锁对象RLock redissonLock = redisson.getLock(lockKey);//加分布式锁redissonLock.lock();try {int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")if (stock > 0) {int realStock = stock - 1;stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)System.out.println("扣减成功,剩余库存:" + realStock);} else {System.out.println("扣减失败,库存不足");}return "end";} finally {redissonLock.unlock();}}}
说明:为啥这种方式可以避免多线程引发的数据问题,由于其中采用LUA脚本的形式,这样能够把一个事务的执行放在LUA脚本中,使得整个事务具备了原子性,同时节约了管道开销
1.7Redis中Lua脚本的使用
1.Redis中的使用
eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
2.java中Redis Lua脚本的使用
package com.redisson;import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;import java.util.Arrays;public class RedisLuaController {public static void main(String[] args) {JedisPoolConfig jedisPoolConfig=new JedisPoolConfig();jedisPoolConfig.setMaxTotal(20);jedisPoolConfig.setMaxIdle(10);jedisPoolConfig.setMinIdle(5);JedisPool jedisPool=new JedisPool(jedisPoolConfig,"192.168.1.61",6379,3000,null);Jedis jedis=null;try{jedis=jedisPool.getResource();jedis.set("product_stock_10016", "15"); //初始化商品10016的库存String script = " local count = redis.call('get', KEYS[1]) " +" local a = tonumber(count) " +" local b = tonumber(ARGV[1]) " +" if a >= b then " +" redis.call('set', KEYS[1], a-b) " +" return 1 " +" end " +" return 0 ";Object obj = jedis.eval(script, Arrays.asList("product_stock_10016"), Arrays.asList("10"));System.out.println(obj);}catch (Exception e){e.printStackTrace();}finally {if(jedis!=null){jedis.close();}}}
}
二.布隆过滤器
对于恶意攻击,向服务器请求大量不存在的数据造成的缓存穿透,还可以用布隆过滤器先做一次过滤,对于不存在的数据布隆过滤器一般都能够过滤掉,不让请求再往后端发送。当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在。
通过一种内置的hash函数进行一个hash运算取整数值,对位数进行取模得到一个值,然后找到对应的位置,把位置的值置为1,当比较的时候就通过同样的算法查看对应的位置是否为1,不为一就直接返回,这样就能够防止内存穿透。